1 <?php
2 3 4 5 6 7 8 9 10 11 12 13 14
15
16 if ( ! defined( 'ABSPATH' ) ) exit;
17
18 require_once ABSPATH . 'wp-admin/includes/admin.php';
19
20 class WC_API_Server {
21
22 const METHOD_GET = 1;
23 const METHOD_POST = 2;
24 const METHOD_PUT = 4;
25 const METHOD_PATCH = 8;
26 const METHOD_DELETE = 16;
27
28 const READABLE = 1;
29 const CREATABLE = 2;
30 const EDITABLE = 14;
31 const DELETABLE = 16;
32 const ALLMETHODS = 31;
33
34 35 36
37 const ACCEPT_RAW_DATA = 64;
38
39
40 const ACCEPT_DATA = 128;
41
42 43 44
45 const HIDDEN_ENDPOINT = 256;
46
47 48 49 50
51 public static $method_map = array(
52 'HEAD' => self::METHOD_GET,
53 'GET' => self::METHOD_GET,
54 'POST' => self::METHOD_POST,
55 'PUT' => self::METHOD_PUT,
56 'PATCH' => self::METHOD_PATCH,
57 'DELETE' => self::METHOD_DELETE,
58 );
59
60 61 62 63 64
65 public $path = '';
66
67 68 69 70 71
72 public $method = 'HEAD';
73
74 75 76 77 78 79 80 81
82 public $params = array( 'GET' => array(), 'POST' => array() );
83
84 85 86 87 88
89 public = array();
90
91 92 93 94 95
96 public $files = array();
97
98 99 100 101 102 103
104 public $handler;
105
106
107 108 109 110 111 112 113
114 public function __construct( $path ) {
115
116 if ( empty( $path ) ) {
117 if ( isset( $_SERVER['PATH_INFO'] ) )
118 $path = $_SERVER['PATH_INFO'];
119 else
120 $path = '/';
121 }
122
123 $this->path = $path;
124 $this->method = $_SERVER['REQUEST_METHOD'];
125 $this->params['GET'] = $_GET;
126 $this->params['POST'] = $_POST;
127 $this->headers = $this->get_headers( $_SERVER );
128 $this->files = $_FILES;
129
130
131 if ( isset( $_GET['_method'] ) ) {
132 $this->method = strtoupper( $_GET['_method'] );
133 }
134
135
136 if ( $this->is_json_request() )
137 $handler_class = 'WC_API_JSON_Handler';
138
139 elseif ( $this->is_xml_request() )
140 $handler_class = 'WC_API_XML_Handler';
141
142 else
143 $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
144
145 $this->handler = new $handler_class();
146 }
147
148 149 150 151 152 153
154 public function check_authentication() {
155
156
157 $user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
158
159
160 if ( is_a( $user, 'WP_User' ) )
161 wp_set_current_user( $user->ID );
162
163
164 elseif ( ! is_wp_error( $user ) )
165 $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
166
167 return $user;
168 }
169
170 171 172 173 174 175 176 177 178 179 180
181 protected function error_to_array( $error ) {
182 $errors = array();
183 foreach ( (array) $error->errors as $code => $messages ) {
184 foreach ( (array) $messages as $message ) {
185 $errors[] = array( 'code' => $code, 'message' => $message );
186 }
187 }
188 return array( 'errors' => $errors );
189 }
190
191 192 193 194 195 196 197 198 199
200 public function serve_request() {
201
202 do_action( 'woocommerce_api_server_before_serve', $this );
203
204 $this->header( 'Content-Type', $this->handler->get_content_type(), true );
205
206
207 if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
208
209 $this->send_status( 404 );
210
211 echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
212
213 return;
214 }
215
216 $result = $this->check_authentication();
217
218
219 if ( ! is_wp_error( $result ) ) {
220 $result = $this->dispatch();
221 }
222
223
224 if ( is_wp_error( $result ) ) {
225 $data = $result->get_error_data();
226 if ( is_array( $data ) && isset( $data['status'] ) ) {
227 $this->send_status( $data['status'] );
228 }
229
230 $result = $this->error_to_array( $result );
231 }
232
233
234
235 $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
236
237 if ( ! $served ) {
238
239 if ( 'HEAD' === $this->method )
240 return;
241
242 echo $this->handler->generate_response( $result );
243 }
244 }
245
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
264 public function get_routes() {
265
266
267 $endpoints = array(
268
269 '/' => array( array( $this, 'get_index' ), self::READABLE ),
270 );
271
272 $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
273
274
275 foreach ( $endpoints as $route => &$handlers ) {
276 if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
277 $handlers = array( $handlers );
278 }
279 }
280
281 return $endpoints;
282 }
283
284 285 286 287 288 289
290 public function dispatch() {
291
292 switch ( $this->method ) {
293
294 case 'HEAD':
295 case 'GET':
296 $method = self::METHOD_GET;
297 break;
298
299 case 'POST':
300 $method = self::METHOD_POST;
301 break;
302
303 case 'PUT':
304 $method = self::METHOD_PUT;
305 break;
306
307 case 'PATCH':
308 $method = self::METHOD_PATCH;
309 break;
310
311 case 'DELETE':
312 $method = self::METHOD_DELETE;
313 break;
314
315 default:
316 return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
317 }
318
319 foreach ( $this->get_routes() as $route => $handlers ) {
320 foreach ( $handlers as $handler ) {
321 $callback = $handler[0];
322 $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
323
324 if ( !( $supported & $method ) )
325 continue;
326
327 $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
328
329 if ( !$match )
330 continue;
331
332 if ( ! is_callable( $callback ) )
333 return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
334
335 $args = array_merge( $args, $this->params['GET'] );
336 if ( $method & self::METHOD_POST ) {
337 $args = array_merge( $args, $this->params['POST'] );
338 }
339 if ( $supported & self::ACCEPT_DATA ) {
340 $data = $this->handler->parse_body( $this->get_raw_data() );
341 $args = array_merge( $args, array( 'data' => $data ) );
342 }
343 elseif ( $supported & self::ACCEPT_RAW_DATA ) {
344 $data = $this->get_raw_data();
345 $args = array_merge( $args, array( 'data' => $data ) );
346 }
347
348 $args['_method'] = $method;
349 $args['_route'] = $route;
350 $args['_path'] = $this->path;
351 $args['_headers'] = $this->headers;
352 $args['_files'] = $this->files;
353
354 $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
355
356
357 if ( is_wp_error( $args ) ) {
358 return $args;
359 }
360
361 $params = $this->sort_callback_params( $callback, $args );
362 if ( is_wp_error( $params ) )
363 return $params;
364
365 return call_user_func_array( $callback, $params );
366 }
367 }
368
369 return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
370 }
371
372 373 374 375 376 377 378 379 380 381 382
383 protected function sort_callback_params( $callback, $provided ) {
384 if ( is_array( $callback ) )
385 $ref_func = new ReflectionMethod( $callback[0], $callback[1] );
386 else
387 $ref_func = new ReflectionFunction( $callback );
388
389 $wanted = $ref_func->getParameters();
390 $ordered_parameters = array();
391
392 foreach ( $wanted as $param ) {
393 if ( isset( $provided[ $param->getName() ] ) ) {
394
395
396 $ordered_parameters[] = is_array( $provided[ $param->getName() ] ) ? array_map( 'urldecode', $provided[ $param->getName() ] ) : urldecode( $provided[ $param->getName() ] );
397 }
398 elseif ( $param->isDefaultValueAvailable() ) {
399
400 $ordered_parameters[] = $param->getDefaultValue();
401 }
402 else {
403
404 return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
405 }
406 }
407 return $ordered_parameters;
408 }
409
410 411 412 413 414 415 416 417
418 public function get_index() {
419
420
421 $available = array( 'store' => array(
422 'name' => get_option( 'blogname' ),
423 'description' => get_option( 'blogdescription' ),
424 'URL' => get_option( 'siteurl' ),
425 'wc_version' => WC()->version,
426 'routes' => array(),
427 'meta' => array(
428 'timezone' => wc_timezone_string(),
429 'currency' => get_woocommerce_currency(),
430 'currency_format' => get_woocommerce_currency_symbol(),
431 'tax_included' => ( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ),
432 'weight_unit' => get_option( 'woocommerce_weight_unit' ),
433 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
434 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
435 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
436 'links' => array(
437 'help' => 'http://woothemes.github.io/woocommerce/rest-api/',
438 ),
439 ),
440 ) );
441
442
443 foreach ( $this->get_routes() as $route => $callbacks ) {
444 $data = array();
445
446 $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
447 $methods = array();
448 foreach ( self::$method_map as $name => $bitmask ) {
449 foreach ( $callbacks as $callback ) {
450
451 if ( $callback[1] & self::HIDDEN_ENDPOINT )
452 continue 3;
453
454 if ( $callback[1] & $bitmask )
455 $data['supports'][] = $name;
456
457 if ( $callback[1] & self::ACCEPT_DATA )
458 $data['accepts_data'] = true;
459
460
461 if ( strpos( $route, '<' ) === false ) {
462 $data['meta'] = array(
463 'self' => get_woocommerce_api_url( $route ),
464 );
465 }
466 }
467 }
468 $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
469 }
470 return apply_filters( 'woocommerce_api_index', $available );
471 }
472
473 474 475 476 477 478
479 public function send_status( $code ) {
480 status_header( $code );
481 }
482
483 484 485 486 487 488 489 490
491 public function header( $key, $value, $replace = true ) {
492 header( sprintf( '%s: %s', $key, $value ), $replace );
493 }
494
495 496 497 498 499 500 501 502 503 504 505 506 507
508 public function ( $rel, $link, $other = array() ) {
509
510 $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
511
512 foreach ( $other as $key => $value ) {
513
514 if ( 'title' == $key ) {
515
516 $value = '"' . $value . '"';
517 }
518
519 $header .= '; ' . $key . '=' . $value;
520 }
521
522 $this->header( 'Link', $header, false );
523 }
524
525 526 527 528 529 530
531 public function ( $query ) {
532
533
534 if ( is_a( $query, 'WP_User_Query' ) ) {
535
536 $page = $query->page;
537 $single = count( $query->get_results() ) > 1;
538 $total = $query->get_total();
539 $total_pages = $query->total_pages;
540
541
542 } else {
543
544 $page = $query->get( 'paged' );
545 $single = $query->is_single();
546 $total = $query->found_posts;
547 $total_pages = $query->max_num_pages;
548 }
549
550 if ( ! $page )
551 $page = 1;
552
553 $next_page = absint( $page ) + 1;
554
555 if ( ! $single ) {
556
557
558 if ( $page > 1 ) {
559 $this->link_header( 'first', $this->get_paginated_url( 1 ) );
560 $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
561 }
562
563
564 if ( $next_page <= $total_pages ) {
565 $this->link_header( 'next', $this->get_paginated_url( $next_page ) );
566 }
567
568
569 if ( $page != $total_pages )
570 $this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
571 }
572
573 $this->header( 'X-WC-Total', $total );
574 $this->header( 'X-WC-TotalPages', $total_pages );
575
576 do_action( 'woocommerce_api_pagination_headers', $this, $query );
577 }
578
579 580 581 582 583 584 585
586 private function get_paginated_url( $page ) {
587
588
589 $request = remove_query_arg( 'page' );
590
591
592 $request = urldecode( add_query_arg( 'page', $page, $request ) );
593
594
595 $host = parse_url( get_home_url(), PHP_URL_HOST );
596
597 return set_url_scheme( "http://{$host}{$request}" );
598 }
599
600 601 602 603 604 605
606 public function get_raw_data() {
607 global $HTTP_RAW_POST_DATA;
608
609
610
611 if ( !isset( $HTTP_RAW_POST_DATA ) ) {
612 $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
613 }
614
615 return $HTTP_RAW_POST_DATA;
616 }
617
618 619 620 621 622 623 624 625 626
627 public function parse_datetime( $datetime ) {
628
629
630 if ( strpos( $datetime, '.' ) !== false ) {
631 $datetime = preg_replace( '/\.\d+/', '', $datetime );
632 }
633
634
635 $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
636
637 try {
638
639 $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
640
641 } catch ( Exception $e ) {
642
643 $datetime = new DateTime( '@0' );
644
645 }
646
647 return $datetime->format( 'Y-m-d H:i:s' );
648 }
649
650 651 652 653 654 655 656 657
658 public function format_datetime( $timestamp, $convert_to_utc = false ) {
659
660 if ( $convert_to_utc ) {
661 $timezone = new DateTimeZone( wc_timezone_string() );
662 } else {
663 $timezone = new DateTimeZone( 'UTC' );
664 }
665
666 try {
667
668 if ( is_numeric( $timestamp ) ) {
669 $date = new DateTime( "@{$timestamp}" );
670 } else {
671 $date = new DateTime( $timestamp, $timezone );
672 }
673
674
675 if ( $convert_to_utc ) {
676 $date->modify( -1 * $date->getOffset() . ' seconds' );
677 }
678
679 } catch ( Exception $e ) {
680
681 $date = new DateTime( '@0' );
682 }
683
684 return $date->format( 'Y-m-d\TH:i:s\Z' );
685 }
686
687 688 689 690 691 692 693
694 public function get_headers($server) {
695 $headers = array();
696
697 $additional = array('CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true);
698
699 foreach ($server as $key => $value) {
700 if ( strpos( $key, 'HTTP_' ) === 0) {
701 $headers[ substr( $key, 5 ) ] = $value;
702 }
703 elseif ( isset( $additional[ $key ] ) ) {
704 $headers[ $key ] = $value;
705 }
706 }
707
708 return $headers;
709 }
710
711 712 713 714 715 716 717
718 private function is_json_request() {
719
720
721 if ( false !== stripos( $this->path, '.json' ) )
722 return true;
723
724
725 if ( isset( $this->headers['ACCEPT'] ) && 'application/json' == $this->headers['ACCEPT'] )
726 return true;
727
728 return false;
729 }
730
731 732 733 734 735 736 737
738 private function is_xml_request() {
739
740
741 if ( false !== stripos( $this->path, '.xml' ) )
742 return true;
743
744
745 if ( isset( $this->headers['ACCEPT'] ) && ( 'application/xml' == $this->headers['ACCEPT'] || 'text/xml' == $this->headers['ACCEPT'] ) )
746 return true;
747
748 return false;
749 }
750 }
751