|
1
|
|
|
<?php |
|
|
|
|
|
|
2
|
|
|
/** |
|
3
|
|
|
* WooCommerce API |
|
4
|
|
|
* |
|
5
|
|
|
* Handles REST API requests |
|
6
|
|
|
* |
|
7
|
|
|
* This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API) |
|
8
|
|
|
* Many thanks to Ryan McCue and any other contributors! |
|
9
|
|
|
* |
|
10
|
|
|
* @author WooThemes |
|
11
|
|
|
* @category API |
|
12
|
|
|
* @package WooCommerce/API |
|
13
|
|
|
* @since 2.1 |
|
14
|
|
|
* @version 2.1 |
|
15
|
|
|
*/ |
|
16
|
|
|
|
|
17
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
|
18
|
|
|
exit; // Exit if accessed directly |
|
19
|
|
|
} |
|
20
|
|
|
|
|
21
|
|
|
require_once ABSPATH . 'wp-admin/includes/admin.php'; |
|
22
|
|
|
|
|
23
|
|
|
class WC_API_Server { |
|
24
|
|
|
|
|
25
|
|
|
const METHOD_GET = 1; |
|
26
|
|
|
const METHOD_POST = 2; |
|
27
|
|
|
const METHOD_PUT = 4; |
|
28
|
|
|
const METHOD_PATCH = 8; |
|
29
|
|
|
const METHOD_DELETE = 16; |
|
30
|
|
|
|
|
31
|
|
|
const READABLE = 1; // GET |
|
32
|
|
|
const CREATABLE = 2; // POST |
|
33
|
|
|
const EDITABLE = 14; // POST | PUT | PATCH |
|
34
|
|
|
const DELETABLE = 16; // DELETE |
|
35
|
|
|
const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* Does the endpoint accept a raw request body? |
|
39
|
|
|
*/ |
|
40
|
|
|
const ACCEPT_RAW_DATA = 64; |
|
41
|
|
|
|
|
42
|
|
|
/** Does the endpoint accept a request body? (either JSON or XML) */ |
|
43
|
|
|
const ACCEPT_DATA = 128; |
|
44
|
|
|
|
|
45
|
|
|
/** |
|
46
|
|
|
* Should we hide this endpoint from the index? |
|
47
|
|
|
*/ |
|
48
|
|
|
const HIDDEN_ENDPOINT = 256; |
|
49
|
|
|
|
|
50
|
|
|
/** |
|
51
|
|
|
* Map of HTTP verbs to constants |
|
52
|
|
|
* @var array |
|
53
|
|
|
*/ |
|
54
|
|
|
public static $method_map = array( |
|
55
|
|
|
'HEAD' => self::METHOD_GET, |
|
56
|
|
|
'GET' => self::METHOD_GET, |
|
57
|
|
|
'POST' => self::METHOD_POST, |
|
58
|
|
|
'PUT' => self::METHOD_PUT, |
|
59
|
|
|
'PATCH' => self::METHOD_PATCH, |
|
60
|
|
|
'DELETE' => self::METHOD_DELETE, |
|
61
|
|
|
); |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* Requested path (relative to the API root, wp-json.php) |
|
65
|
|
|
* |
|
66
|
|
|
* @var string |
|
67
|
|
|
*/ |
|
68
|
|
|
public $path = ''; |
|
69
|
|
|
|
|
70
|
|
|
/** |
|
71
|
|
|
* Requested method (GET/HEAD/POST/PUT/PATCH/DELETE) |
|
72
|
|
|
* |
|
73
|
|
|
* @var string |
|
74
|
|
|
*/ |
|
75
|
|
|
public $method = 'HEAD'; |
|
76
|
|
|
|
|
77
|
|
|
/** |
|
78
|
|
|
* Request parameters |
|
79
|
|
|
* |
|
80
|
|
|
* This acts as an abstraction of the superglobals |
|
81
|
|
|
* (GET => $_GET, POST => $_POST) |
|
82
|
|
|
* |
|
83
|
|
|
* @var array |
|
84
|
|
|
*/ |
|
85
|
|
|
public $params = array( 'GET' => array(), 'POST' => array() ); |
|
86
|
|
|
|
|
87
|
|
|
/** |
|
88
|
|
|
* Request headers |
|
89
|
|
|
* |
|
90
|
|
|
* @var array |
|
91
|
|
|
*/ |
|
92
|
|
|
public $headers = array(); |
|
93
|
|
|
|
|
94
|
|
|
/** |
|
95
|
|
|
* Request files (matches $_FILES) |
|
96
|
|
|
* |
|
97
|
|
|
* @var array |
|
98
|
|
|
*/ |
|
99
|
|
|
public $files = array(); |
|
100
|
|
|
|
|
101
|
|
|
/** |
|
102
|
|
|
* Request/Response handler, either JSON by default |
|
103
|
|
|
* or XML if requested by client |
|
104
|
|
|
* |
|
105
|
|
|
* @var WC_API_Handler |
|
106
|
|
|
*/ |
|
107
|
|
|
public $handler; |
|
108
|
|
|
|
|
109
|
|
|
|
|
110
|
|
|
/** |
|
111
|
|
|
* Setup class and set request/response handler |
|
112
|
|
|
* |
|
113
|
|
|
* @since 2.1 |
|
114
|
|
|
* @param $path |
|
115
|
|
|
* @return WC_API_Server |
|
|
|
|
|
|
116
|
|
|
*/ |
|
117
|
|
|
public function __construct( $path ) { |
|
118
|
|
|
|
|
119
|
|
|
if ( empty( $path ) ) { |
|
120
|
|
|
if ( isset( $_SERVER['PATH_INFO'] ) ) |
|
121
|
|
|
$path = $_SERVER['PATH_INFO']; |
|
122
|
|
|
else |
|
123
|
|
|
$path = '/'; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
$this->path = $path; |
|
127
|
|
|
$this->method = $_SERVER['REQUEST_METHOD']; |
|
128
|
|
|
$this->params['GET'] = $_GET; |
|
129
|
|
|
$this->params['POST'] = $_POST; |
|
130
|
|
|
$this->headers = $this->get_headers( $_SERVER ); |
|
131
|
|
|
$this->files = $_FILES; |
|
132
|
|
|
|
|
133
|
|
|
// Compatibility for clients that can't use PUT/PATCH/DELETE |
|
134
|
|
|
if ( isset( $_GET['_method'] ) ) { |
|
135
|
|
|
$this->method = strtoupper( $_GET['_method'] ); |
|
136
|
|
|
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) { |
|
137
|
|
|
$this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
// determine type of request/response and load handler, JSON by default |
|
141
|
|
|
if ( $this->is_json_request() ) |
|
142
|
|
|
$handler_class = 'WC_API_JSON_Handler'; |
|
143
|
|
|
|
|
144
|
|
|
elseif ( $this->is_xml_request() ) |
|
145
|
|
|
$handler_class = 'WC_API_XML_Handler'; |
|
146
|
|
|
|
|
147
|
|
|
else |
|
148
|
|
|
$handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this ); |
|
149
|
|
|
|
|
150
|
|
|
$this->handler = new $handler_class(); |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Check authentication for the request |
|
155
|
|
|
* |
|
156
|
|
|
* @since 2.1 |
|
157
|
|
|
* @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login |
|
158
|
|
|
*/ |
|
159
|
|
View Code Duplication |
public function check_authentication() { |
|
|
|
|
|
|
160
|
|
|
|
|
161
|
|
|
// allow plugins to remove default authentication or add their own authentication |
|
162
|
|
|
$user = apply_filters( 'woocommerce_api_check_authentication', null, $this ); |
|
163
|
|
|
|
|
164
|
|
|
// API requests run under the context of the authenticated user |
|
165
|
|
|
if ( is_a( $user, 'WP_User' ) ) |
|
166
|
|
|
wp_set_current_user( $user->ID ); |
|
167
|
|
|
|
|
168
|
|
|
// WP_Errors are handled in serve_request() |
|
169
|
|
|
elseif ( ! is_wp_error( $user ) ) |
|
170
|
|
|
$user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) ); |
|
171
|
|
|
|
|
172
|
|
|
return $user; |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* Convert an error to an array |
|
177
|
|
|
* |
|
178
|
|
|
* This iterates over all error codes and messages to change it into a flat |
|
179
|
|
|
* array. This enables simpler client behaviour, as it is represented as a |
|
180
|
|
|
* list in JSON rather than an object/map |
|
181
|
|
|
* |
|
182
|
|
|
* @since 2.1 |
|
183
|
|
|
* @param WP_Error $error |
|
184
|
|
|
* @return array List of associative arrays with code and message keys |
|
185
|
|
|
*/ |
|
186
|
|
View Code Duplication |
protected function error_to_array( $error ) { |
|
|
|
|
|
|
187
|
|
|
$errors = array(); |
|
188
|
|
|
foreach ( (array) $error->errors as $code => $messages ) { |
|
189
|
|
|
foreach ( (array) $messages as $message ) { |
|
190
|
|
|
$errors[] = array( 'code' => $code, 'message' => $message ); |
|
191
|
|
|
} |
|
192
|
|
|
} |
|
193
|
|
|
return array( 'errors' => $errors ); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
/** |
|
197
|
|
|
* Handle serving an API request |
|
198
|
|
|
* |
|
199
|
|
|
* Matches the current server URI to a route and runs the first matching |
|
200
|
|
|
* callback then outputs a JSON representation of the returned value. |
|
201
|
|
|
* |
|
202
|
|
|
* @since 2.1 |
|
203
|
|
|
* @uses WC_API_Server::dispatch() |
|
204
|
|
|
*/ |
|
205
|
|
View Code Duplication |
public function serve_request() { |
|
|
|
|
|
|
206
|
|
|
|
|
207
|
|
|
do_action( 'woocommerce_api_server_before_serve', $this ); |
|
208
|
|
|
|
|
209
|
|
|
$this->header( 'Content-Type', $this->handler->get_content_type(), true ); |
|
210
|
|
|
|
|
211
|
|
|
// the API is enabled by default |
|
212
|
|
|
if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) { |
|
213
|
|
|
|
|
214
|
|
|
$this->send_status( 404 ); |
|
215
|
|
|
|
|
216
|
|
|
echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) ); |
|
217
|
|
|
|
|
218
|
|
|
return; |
|
219
|
|
|
} |
|
220
|
|
|
|
|
221
|
|
|
$result = $this->check_authentication(); |
|
222
|
|
|
|
|
223
|
|
|
// if authorization check was successful, dispatch the request |
|
224
|
|
|
if ( ! is_wp_error( $result ) ) { |
|
225
|
|
|
$result = $this->dispatch(); |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
// handle any dispatch errors |
|
229
|
|
|
if ( is_wp_error( $result ) ) { |
|
230
|
|
|
$data = $result->get_error_data(); |
|
231
|
|
|
if ( is_array( $data ) && isset( $data['status'] ) ) { |
|
232
|
|
|
$this->send_status( $data['status'] ); |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
$result = $this->error_to_array( $result ); |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
// This is a filter rather than an action, since this is designed to be |
|
239
|
|
|
// re-entrant if needed |
|
240
|
|
|
$served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this ); |
|
241
|
|
|
|
|
242
|
|
|
if ( ! $served ) { |
|
243
|
|
|
|
|
244
|
|
|
if ( 'HEAD' === $this->method ) |
|
245
|
|
|
return; |
|
246
|
|
|
|
|
247
|
|
|
echo $this->handler->generate_response( $result ); |
|
248
|
|
|
} |
|
249
|
|
|
} |
|
250
|
|
|
|
|
251
|
|
|
/** |
|
252
|
|
|
* Retrieve the route map |
|
253
|
|
|
* |
|
254
|
|
|
* The route map is an associative array with path regexes as the keys. The |
|
255
|
|
|
* value is an indexed array with the callback function/method as the first |
|
256
|
|
|
* item, and a bitmask of HTTP methods as the second item (see the class |
|
257
|
|
|
* constants). |
|
258
|
|
|
* |
|
259
|
|
|
* Each route can be mapped to more than one callback by using an array of |
|
260
|
|
|
* the indexed arrays. This allows mapping e.g. GET requests to one callback |
|
261
|
|
|
* and POST requests to another. |
|
262
|
|
|
* |
|
263
|
|
|
* Note that the path regexes (array keys) must have @ escaped, as this is |
|
264
|
|
|
* used as the delimiter with preg_match() |
|
265
|
|
|
* |
|
266
|
|
|
* @since 2.1 |
|
267
|
|
|
* @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)` |
|
268
|
|
|
*/ |
|
269
|
|
View Code Duplication |
public function get_routes() { |
|
|
|
|
|
|
270
|
|
|
|
|
271
|
|
|
// index added by default |
|
272
|
|
|
$endpoints = array( |
|
273
|
|
|
|
|
274
|
|
|
'/' => array( array( $this, 'get_index' ), self::READABLE ), |
|
275
|
|
|
); |
|
276
|
|
|
|
|
277
|
|
|
$endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints ); |
|
278
|
|
|
|
|
279
|
|
|
// Normalise the endpoints |
|
280
|
|
|
foreach ( $endpoints as $route => &$handlers ) { |
|
281
|
|
|
if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) { |
|
282
|
|
|
$handlers = array( $handlers ); |
|
283
|
|
|
} |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
return $endpoints; |
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
/** |
|
290
|
|
|
* Match the request to a callback and call it |
|
291
|
|
|
* |
|
292
|
|
|
* @since 2.1 |
|
293
|
|
|
* @return mixed The value returned by the callback, or a WP_Error instance |
|
294
|
|
|
*/ |
|
295
|
|
View Code Duplication |
public function dispatch() { |
|
|
|
|
|
|
296
|
|
|
|
|
297
|
|
|
switch ( $this->method ) { |
|
298
|
|
|
|
|
299
|
|
|
case 'HEAD': |
|
300
|
|
|
case 'GET': |
|
301
|
|
|
$method = self::METHOD_GET; |
|
302
|
|
|
break; |
|
303
|
|
|
|
|
304
|
|
|
case 'POST': |
|
305
|
|
|
$method = self::METHOD_POST; |
|
306
|
|
|
break; |
|
307
|
|
|
|
|
308
|
|
|
case 'PUT': |
|
309
|
|
|
$method = self::METHOD_PUT; |
|
310
|
|
|
break; |
|
311
|
|
|
|
|
312
|
|
|
case 'PATCH': |
|
313
|
|
|
$method = self::METHOD_PATCH; |
|
314
|
|
|
break; |
|
315
|
|
|
|
|
316
|
|
|
case 'DELETE': |
|
317
|
|
|
$method = self::METHOD_DELETE; |
|
318
|
|
|
break; |
|
319
|
|
|
|
|
320
|
|
|
default: |
|
321
|
|
|
return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) ); |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
foreach ( $this->get_routes() as $route => $handlers ) { |
|
325
|
|
|
foreach ( $handlers as $handler ) { |
|
326
|
|
|
$callback = $handler[0]; |
|
327
|
|
|
$supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET; |
|
328
|
|
|
|
|
329
|
|
|
if ( !( $supported & $method ) ) |
|
330
|
|
|
continue; |
|
331
|
|
|
|
|
332
|
|
|
$match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args ); |
|
333
|
|
|
|
|
334
|
|
|
if ( !$match ) |
|
335
|
|
|
continue; |
|
336
|
|
|
|
|
337
|
|
|
if ( ! is_callable( $callback ) ) |
|
338
|
|
|
return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) ); |
|
339
|
|
|
|
|
340
|
|
|
$args = array_merge( $args, $this->params['GET'] ); |
|
341
|
|
|
if ( $method & self::METHOD_POST ) { |
|
342
|
|
|
$args = array_merge( $args, $this->params['POST'] ); |
|
343
|
|
|
} |
|
344
|
|
|
if ( $supported & self::ACCEPT_DATA ) { |
|
345
|
|
|
$data = $this->handler->parse_body( $this->get_raw_data() ); |
|
346
|
|
|
$args = array_merge( $args, array( 'data' => $data ) ); |
|
347
|
|
|
} |
|
348
|
|
|
elseif ( $supported & self::ACCEPT_RAW_DATA ) { |
|
349
|
|
|
$data = $this->get_raw_data(); |
|
350
|
|
|
$args = array_merge( $args, array( 'data' => $data ) ); |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
$args['_method'] = $method; |
|
354
|
|
|
$args['_route'] = $route; |
|
355
|
|
|
$args['_path'] = $this->path; |
|
356
|
|
|
$args['_headers'] = $this->headers; |
|
357
|
|
|
$args['_files'] = $this->files; |
|
358
|
|
|
|
|
359
|
|
|
$args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback ); |
|
360
|
|
|
|
|
361
|
|
|
// Allow plugins to halt the request via this filter |
|
362
|
|
|
if ( is_wp_error( $args ) ) { |
|
363
|
|
|
return $args; |
|
364
|
|
|
} |
|
365
|
|
|
|
|
366
|
|
|
$params = $this->sort_callback_params( $callback, $args ); |
|
367
|
|
|
if ( is_wp_error( $params ) ) |
|
368
|
|
|
return $params; |
|
369
|
|
|
|
|
370
|
|
|
return call_user_func_array( $callback, $params ); |
|
371
|
|
|
} |
|
372
|
|
|
} |
|
373
|
|
|
|
|
374
|
|
|
return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) ); |
|
375
|
|
|
} |
|
376
|
|
|
|
|
377
|
|
|
/** |
|
378
|
|
|
* Sort parameters by order specified in method declaration |
|
379
|
|
|
* |
|
380
|
|
|
* Takes a callback and a list of available params, then filters and sorts |
|
381
|
|
|
* by the parameters the method actually needs, using the Reflection API |
|
382
|
|
|
* |
|
383
|
|
|
* @since 2.1 |
|
384
|
|
|
* @param callable|array $callback the endpoint callback |
|
385
|
|
|
* @param array $provided the provided request parameters |
|
386
|
|
|
* @return array |
|
387
|
|
|
*/ |
|
388
|
|
|
protected function sort_callback_params( $callback, $provided ) { |
|
389
|
|
|
if ( is_array( $callback ) ) |
|
390
|
|
|
$ref_func = new ReflectionMethod( $callback[0], $callback[1] ); |
|
391
|
|
|
else |
|
392
|
|
|
$ref_func = new ReflectionFunction( $callback ); |
|
393
|
|
|
|
|
394
|
|
|
$wanted = $ref_func->getParameters(); |
|
395
|
|
|
$ordered_parameters = array(); |
|
396
|
|
|
|
|
397
|
|
|
foreach ( $wanted as $param ) { |
|
398
|
|
|
if ( isset( $provided[ $param->getName() ] ) ) { |
|
399
|
|
|
// We have this parameters in the list to choose from |
|
400
|
|
|
|
|
401
|
|
|
$ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] ); |
|
402
|
|
|
} |
|
403
|
|
|
elseif ( $param->isDefaultValueAvailable() ) { |
|
404
|
|
|
// We don't have this parameter, but it's optional |
|
405
|
|
|
$ordered_parameters[] = $param->getDefaultValue(); |
|
406
|
|
|
} |
|
407
|
|
|
else { |
|
408
|
|
|
// We don't have this parameter and it wasn't optional, abort! |
|
409
|
|
|
return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) ); |
|
410
|
|
|
} |
|
411
|
|
|
} |
|
412
|
|
|
return $ordered_parameters; |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
/** |
|
416
|
|
|
* Get the site index. |
|
417
|
|
|
* |
|
418
|
|
|
* This endpoint describes the capabilities of the site. |
|
419
|
|
|
* |
|
420
|
|
|
* @since 2.1 |
|
421
|
|
|
* @return array Index entity |
|
422
|
|
|
*/ |
|
423
|
|
|
public function get_index() { |
|
424
|
|
|
|
|
425
|
|
|
// General site data |
|
426
|
|
|
$available = array( 'store' => array( |
|
427
|
|
|
'name' => get_option( 'blogname' ), |
|
428
|
|
|
'description' => get_option( 'blogdescription' ), |
|
429
|
|
|
'URL' => get_option( 'siteurl' ), |
|
430
|
|
|
'wc_version' => WC()->version, |
|
431
|
|
|
'routes' => array(), |
|
432
|
|
|
'meta' => array( |
|
433
|
|
|
'timezone' => wc_timezone_string(), |
|
434
|
|
|
'currency' => get_woocommerce_currency(), |
|
435
|
|
|
'currency_format' => get_woocommerce_currency_symbol(), |
|
436
|
|
|
'tax_included' => wc_prices_include_tax(), |
|
437
|
|
|
'weight_unit' => get_option( 'woocommerce_weight_unit' ), |
|
438
|
|
|
'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), |
|
439
|
|
|
'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ), |
|
440
|
|
|
'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ), |
|
441
|
|
|
'links' => array( |
|
442
|
|
|
'help' => 'http://woothemes.github.io/woocommerce/rest-api/', |
|
443
|
|
|
), |
|
444
|
|
|
), |
|
445
|
|
|
) ); |
|
446
|
|
|
|
|
447
|
|
|
// Find the available routes |
|
448
|
|
|
foreach ( $this->get_routes() as $route => $callbacks ) { |
|
449
|
|
|
$data = array(); |
|
450
|
|
|
|
|
451
|
|
|
$route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route ); |
|
452
|
|
|
$methods = array(); |
|
|
|
|
|
|
453
|
|
|
foreach ( self::$method_map as $name => $bitmask ) { |
|
454
|
|
|
foreach ( $callbacks as $callback ) { |
|
455
|
|
|
// Skip to the next route if any callback is hidden |
|
456
|
|
|
if ( $callback[1] & self::HIDDEN_ENDPOINT ) |
|
457
|
|
|
continue 3; |
|
458
|
|
|
|
|
459
|
|
|
if ( $callback[1] & $bitmask ) |
|
460
|
|
|
$data['supports'][] = $name; |
|
461
|
|
|
|
|
462
|
|
|
if ( $callback[1] & self::ACCEPT_DATA ) |
|
463
|
|
|
$data['accepts_data'] = true; |
|
464
|
|
|
|
|
465
|
|
|
// For non-variable routes, generate links |
|
466
|
|
|
if ( strpos( $route, '<' ) === false ) { |
|
467
|
|
|
$data['meta'] = array( |
|
468
|
|
|
'self' => get_woocommerce_api_url( $route ), |
|
469
|
|
|
); |
|
470
|
|
|
} |
|
471
|
|
|
} |
|
472
|
|
|
} |
|
473
|
|
|
$available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data ); |
|
474
|
|
|
} |
|
475
|
|
|
return apply_filters( 'woocommerce_api_index', $available ); |
|
476
|
|
|
} |
|
477
|
|
|
|
|
478
|
|
|
/** |
|
479
|
|
|
* Send a HTTP status code |
|
480
|
|
|
* |
|
481
|
|
|
* @since 2.1 |
|
482
|
|
|
* @param int $code HTTP status |
|
483
|
|
|
*/ |
|
484
|
|
|
public function send_status( $code ) { |
|
485
|
|
|
status_header( $code ); |
|
486
|
|
|
} |
|
487
|
|
|
|
|
488
|
|
|
/** |
|
489
|
|
|
* Send a HTTP header |
|
490
|
|
|
* |
|
491
|
|
|
* @since 2.1 |
|
492
|
|
|
* @param string $key Header key |
|
493
|
|
|
* @param string $value Header value |
|
494
|
|
|
* @param boolean $replace Should we replace the existing header? |
|
495
|
|
|
*/ |
|
496
|
|
|
public function header( $key, $value, $replace = true ) { |
|
497
|
|
|
header( sprintf( '%s: %s', $key, $value ), $replace ); |
|
498
|
|
|
} |
|
499
|
|
|
|
|
500
|
|
|
/** |
|
501
|
|
|
* Send a Link header |
|
502
|
|
|
* |
|
503
|
|
|
* @internal The $rel parameter is first, as this looks nicer when sending multiple |
|
504
|
|
|
* |
|
505
|
|
|
* @link http://tools.ietf.org/html/rfc5988 |
|
506
|
|
|
* @link http://www.iana.org/assignments/link-relations/link-relations.xml |
|
507
|
|
|
* |
|
508
|
|
|
* @since 2.1 |
|
509
|
|
|
* @param string $rel Link relation. Either a registered type, or an absolute URL |
|
510
|
|
|
* @param string $link Target IRI for the link |
|
511
|
|
|
* @param array $other Other parameters to send, as an associative array |
|
512
|
|
|
*/ |
|
513
|
|
View Code Duplication |
public function link_header( $rel, $link, $other = array() ) { |
|
|
|
|
|
|
514
|
|
|
|
|
515
|
|
|
$header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) ); |
|
516
|
|
|
|
|
517
|
|
|
foreach ( $other as $key => $value ) { |
|
518
|
|
|
|
|
519
|
|
|
if ( 'title' == $key ) { |
|
520
|
|
|
|
|
521
|
|
|
$value = '"' . $value . '"'; |
|
522
|
|
|
} |
|
523
|
|
|
|
|
524
|
|
|
$header .= '; ' . $key . '=' . $value; |
|
525
|
|
|
} |
|
526
|
|
|
|
|
527
|
|
|
$this->header( 'Link', $header, false ); |
|
528
|
|
|
} |
|
529
|
|
|
|
|
530
|
|
|
/** |
|
531
|
|
|
* Send pagination headers for resources |
|
532
|
|
|
* |
|
533
|
|
|
* @since 2.1 |
|
534
|
|
|
* @param WP_Query|WP_User_Query $query |
|
535
|
|
|
*/ |
|
536
|
|
|
public function add_pagination_headers( $query ) { |
|
537
|
|
|
|
|
538
|
|
|
// WP_User_Query |
|
539
|
|
|
if ( is_a( $query, 'WP_User_Query' ) ) { |
|
540
|
|
|
|
|
541
|
|
|
$page = $query->page; |
|
542
|
|
|
$single = count( $query->get_results() ) == 1; |
|
543
|
|
|
$total = $query->get_total(); |
|
544
|
|
|
$total_pages = $query->total_pages; |
|
545
|
|
|
|
|
546
|
|
|
// WP_Query |
|
547
|
|
View Code Duplication |
} else { |
|
|
|
|
|
|
548
|
|
|
|
|
549
|
|
|
$page = $query->get( 'paged' ); |
|
550
|
|
|
$single = $query->is_single(); |
|
551
|
|
|
$total = $query->found_posts; |
|
552
|
|
|
$total_pages = $query->max_num_pages; |
|
553
|
|
|
} |
|
554
|
|
|
|
|
555
|
|
|
if ( ! $page ) |
|
556
|
|
|
$page = 1; |
|
557
|
|
|
|
|
558
|
|
|
$next_page = absint( $page ) + 1; |
|
559
|
|
|
|
|
560
|
|
View Code Duplication |
if ( ! $single ) { |
|
|
|
|
|
|
561
|
|
|
|
|
562
|
|
|
// first/prev |
|
563
|
|
|
if ( $page > 1 ) { |
|
564
|
|
|
$this->link_header( 'first', $this->get_paginated_url( 1 ) ); |
|
565
|
|
|
$this->link_header( 'prev', $this->get_paginated_url( $page -1 ) ); |
|
566
|
|
|
} |
|
567
|
|
|
|
|
568
|
|
|
// next |
|
569
|
|
|
if ( $next_page <= $total_pages ) { |
|
570
|
|
|
$this->link_header( 'next', $this->get_paginated_url( $next_page ) ); |
|
571
|
|
|
} |
|
572
|
|
|
|
|
573
|
|
|
// last |
|
574
|
|
|
if ( $page != $total_pages ) |
|
575
|
|
|
$this->link_header( 'last', $this->get_paginated_url( $total_pages ) ); |
|
576
|
|
|
} |
|
577
|
|
|
|
|
578
|
|
|
$this->header( 'X-WC-Total', $total ); |
|
579
|
|
|
$this->header( 'X-WC-TotalPages', $total_pages ); |
|
580
|
|
|
|
|
581
|
|
|
do_action( 'woocommerce_api_pagination_headers', $this, $query ); |
|
582
|
|
|
} |
|
583
|
|
|
|
|
584
|
|
|
/** |
|
585
|
|
|
* Returns the request URL with the page query parameter set to the specified page |
|
586
|
|
|
* |
|
587
|
|
|
* @since 2.1 |
|
588
|
|
|
* @param int $page |
|
589
|
|
|
* @return string |
|
590
|
|
|
*/ |
|
591
|
|
View Code Duplication |
private function get_paginated_url( $page ) { |
|
|
|
|
|
|
592
|
|
|
|
|
593
|
|
|
// remove existing page query param |
|
594
|
|
|
$request = remove_query_arg( 'page' ); |
|
595
|
|
|
|
|
596
|
|
|
// add provided page query param |
|
597
|
|
|
$request = urldecode( add_query_arg( 'page', $page, $request ) ); |
|
598
|
|
|
|
|
599
|
|
|
// get the home host |
|
600
|
|
|
$host = parse_url( get_home_url(), PHP_URL_HOST ); |
|
601
|
|
|
|
|
602
|
|
|
return set_url_scheme( "http://{$host}{$request}" ); |
|
603
|
|
|
} |
|
604
|
|
|
|
|
605
|
|
|
/** |
|
606
|
|
|
* Retrieve the raw request entity (body) |
|
607
|
|
|
* |
|
608
|
|
|
* @since 2.1 |
|
609
|
|
|
* @return string |
|
610
|
|
|
*/ |
|
611
|
|
View Code Duplication |
public function get_raw_data() { |
|
|
|
|
|
|
612
|
|
|
// $HTTP_RAW_POST_DATA is deprecated on PHP 5.6 |
|
613
|
|
|
if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) { |
|
614
|
|
|
return file_get_contents( 'php://input' ); |
|
615
|
|
|
} |
|
616
|
|
|
|
|
617
|
|
|
global $HTTP_RAW_POST_DATA; |
|
618
|
|
|
|
|
619
|
|
|
// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default, |
|
620
|
|
|
// but we can do it ourself. |
|
621
|
|
|
if ( ! isset( $HTTP_RAW_POST_DATA ) ) { |
|
622
|
|
|
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' ); |
|
623
|
|
|
} |
|
624
|
|
|
|
|
625
|
|
|
return $HTTP_RAW_POST_DATA; |
|
626
|
|
|
} |
|
627
|
|
|
|
|
628
|
|
|
/** |
|
629
|
|
|
* Parse an RFC3339 datetime into a MySQl datetime |
|
630
|
|
|
* |
|
631
|
|
|
* Invalid dates default to unix epoch |
|
632
|
|
|
* |
|
633
|
|
|
* @since 2.1 |
|
634
|
|
|
* @param string $datetime RFC3339 datetime |
|
635
|
|
|
* @return string MySQl datetime (YYYY-MM-DD HH:MM:SS) |
|
636
|
|
|
*/ |
|
637
|
|
View Code Duplication |
public function parse_datetime( $datetime ) { |
|
|
|
|
|
|
638
|
|
|
|
|
639
|
|
|
// Strip millisecond precision (a full stop followed by one or more digits) |
|
640
|
|
|
if ( strpos( $datetime, '.' ) !== false ) { |
|
641
|
|
|
$datetime = preg_replace( '/\.\d+/', '', $datetime ); |
|
642
|
|
|
} |
|
643
|
|
|
|
|
644
|
|
|
// default timezone to UTC |
|
645
|
|
|
$datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime ); |
|
646
|
|
|
|
|
647
|
|
|
try { |
|
648
|
|
|
|
|
649
|
|
|
$datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) ); |
|
650
|
|
|
|
|
651
|
|
|
} catch ( Exception $e ) { |
|
652
|
|
|
|
|
653
|
|
|
$datetime = new DateTime( '@0' ); |
|
654
|
|
|
|
|
655
|
|
|
} |
|
656
|
|
|
|
|
657
|
|
|
return $datetime->format( 'Y-m-d H:i:s' ); |
|
658
|
|
|
} |
|
659
|
|
|
|
|
660
|
|
|
/** |
|
661
|
|
|
* Format a unix timestamp or MySQL datetime into an RFC3339 datetime |
|
662
|
|
|
* |
|
663
|
|
|
* @since 2.1 |
|
664
|
|
|
* @param int|string $timestamp unix timestamp or MySQL datetime |
|
665
|
|
|
* @param bool $convert_to_utc |
|
666
|
|
|
* @return string RFC3339 datetime |
|
667
|
|
|
*/ |
|
668
|
|
View Code Duplication |
public function format_datetime( $timestamp, $convert_to_utc = false ) { |
|
|
|
|
|
|
669
|
|
|
|
|
670
|
|
|
if ( $convert_to_utc ) { |
|
671
|
|
|
$timezone = new DateTimeZone( wc_timezone_string() ); |
|
672
|
|
|
} else { |
|
673
|
|
|
$timezone = new DateTimeZone( 'UTC' ); |
|
674
|
|
|
} |
|
675
|
|
|
|
|
676
|
|
|
try { |
|
677
|
|
|
|
|
678
|
|
|
if ( is_numeric( $timestamp ) ) { |
|
679
|
|
|
$date = new DateTime( "@{$timestamp}" ); |
|
680
|
|
|
} else { |
|
681
|
|
|
$date = new DateTime( $timestamp, $timezone ); |
|
682
|
|
|
} |
|
683
|
|
|
|
|
684
|
|
|
// convert to UTC by adjusting the time based on the offset of the site's timezone |
|
685
|
|
|
if ( $convert_to_utc ) { |
|
686
|
|
|
$date->modify( -1 * $date->getOffset() . ' seconds' ); |
|
687
|
|
|
} |
|
688
|
|
|
|
|
689
|
|
|
} catch ( Exception $e ) { |
|
690
|
|
|
|
|
691
|
|
|
$date = new DateTime( '@0' ); |
|
692
|
|
|
} |
|
693
|
|
|
|
|
694
|
|
|
return $date->format( 'Y-m-d\TH:i:s\Z' ); |
|
695
|
|
|
} |
|
696
|
|
|
|
|
697
|
|
|
/** |
|
698
|
|
|
* Extract headers from a PHP-style $_SERVER array |
|
699
|
|
|
* |
|
700
|
|
|
* @since 2.1 |
|
701
|
|
|
* @param array $server Associative array similar to $_SERVER |
|
702
|
|
|
* @return array Headers extracted from the input |
|
703
|
|
|
*/ |
|
704
|
|
View Code Duplication |
public function get_headers($server) { |
|
|
|
|
|
|
705
|
|
|
$headers = array(); |
|
706
|
|
|
// CONTENT_* headers are not prefixed with HTTP_ |
|
707
|
|
|
$additional = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true); |
|
708
|
|
|
|
|
709
|
|
|
foreach ($server as $key => $value) { |
|
710
|
|
|
if ( strpos( $key, 'HTTP_' ) === 0) { |
|
711
|
|
|
$headers[ substr( $key, 5 ) ] = $value; |
|
712
|
|
|
} |
|
713
|
|
|
elseif ( isset( $additional[ $key ] ) ) { |
|
714
|
|
|
$headers[ $key ] = $value; |
|
715
|
|
|
} |
|
716
|
|
|
} |
|
717
|
|
|
|
|
718
|
|
|
return $headers; |
|
719
|
|
|
} |
|
720
|
|
|
|
|
721
|
|
|
/** |
|
722
|
|
|
* Check if the current request accepts a JSON response by checking the endpoint suffix (.json) or |
|
723
|
|
|
* the HTTP ACCEPT header |
|
724
|
|
|
* |
|
725
|
|
|
* @since 2.1 |
|
726
|
|
|
* @return bool |
|
727
|
|
|
*/ |
|
728
|
|
View Code Duplication |
private function is_json_request() { |
|
|
|
|
|
|
729
|
|
|
|
|
730
|
|
|
// check path |
|
731
|
|
|
if ( false !== stripos( $this->path, '.json' ) ) |
|
732
|
|
|
return true; |
|
733
|
|
|
|
|
734
|
|
|
// check ACCEPT header, only 'application/json' is acceptable, see RFC 4627 |
|
735
|
|
|
if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] ) |
|
736
|
|
|
return true; |
|
737
|
|
|
|
|
738
|
|
|
return false; |
|
739
|
|
|
} |
|
740
|
|
|
|
|
741
|
|
|
/** |
|
742
|
|
|
* Check if the current request accepts an XML response by checking the endpoint suffix (.xml) or |
|
743
|
|
|
* the HTTP ACCEPT header |
|
744
|
|
|
* |
|
745
|
|
|
* @since 2.1 |
|
746
|
|
|
* @return bool |
|
747
|
|
|
*/ |
|
748
|
|
View Code Duplication |
private function is_xml_request() { |
|
|
|
|
|
|
749
|
|
|
|
|
750
|
|
|
// check path |
|
751
|
|
|
if ( false !== stripos( $this->path, '.xml' ) ) |
|
752
|
|
|
return true; |
|
753
|
|
|
|
|
754
|
|
|
// check headers, 'application/xml' or 'text/xml' are acceptable, see RFC 2376 |
|
755
|
|
|
if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) ) |
|
756
|
|
|
return true; |
|
757
|
|
|
|
|
758
|
|
|
return false; |
|
759
|
|
|
} |
|
760
|
|
|
} |
|
761
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.