WC_API_Server

The WooCommerce WC API Server class.

Defined (3)

The class is defined in the following location(s).

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