class-wc-api-server.php 21 KB

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