/includes/api/legacy/v3/class-wc-api-server.php

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