| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775 |
- <?php
- /**
- * WooCommerce API
- *
- * Handles REST API requests
- *
- * This class and related code (JSON response handler, resource classes) are based on WP-API v0.6 (https://github.com/WP-API/WP-API)
- * Many thanks to Ryan McCue and any other contributors!
- *
- * @author WooThemes
- * @category API
- * @package WooCommerce/API
- * @since 2.1
- */
- if ( ! defined( 'ABSPATH' ) ) {
- exit; // Exit if accessed directly
- }
- require_once ABSPATH . 'wp-admin/includes/admin.php';
- class WC_API_Server {
- const METHOD_GET = 1;
- const METHOD_POST = 2;
- const METHOD_PUT = 4;
- const METHOD_PATCH = 8;
- const METHOD_DELETE = 16;
- const READABLE = 1; // GET
- const CREATABLE = 2; // POST
- const EDITABLE = 14; // POST | PUT | PATCH
- const DELETABLE = 16; // DELETE
- const ALLMETHODS = 31; // GET | POST | PUT | PATCH | DELETE
- /**
- * Does the endpoint accept a raw request body?
- */
- const ACCEPT_RAW_DATA = 64;
- /** Does the endpoint accept a request body? (either JSON or XML) */
- const ACCEPT_DATA = 128;
- /**
- * Should we hide this endpoint from the index?
- */
- const HIDDEN_ENDPOINT = 256;
- /**
- * Map of HTTP verbs to constants
- * @var array
- */
- public static $method_map = array(
- 'HEAD' => self::METHOD_GET,
- 'GET' => self::METHOD_GET,
- 'POST' => self::METHOD_POST,
- 'PUT' => self::METHOD_PUT,
- 'PATCH' => self::METHOD_PATCH,
- 'DELETE' => self::METHOD_DELETE,
- );
- /**
- * Requested path (relative to the API root, wp-json.php)
- *
- * @var string
- */
- public $path = '';
- /**
- * Requested method (GET/HEAD/POST/PUT/PATCH/DELETE)
- *
- * @var string
- */
- public $method = 'HEAD';
- /**
- * Request parameters
- *
- * This acts as an abstraction of the superglobals
- * (GET => $_GET, POST => $_POST)
- *
- * @var array
- */
- public $params = array( 'GET' => array(), 'POST' => array() );
- /**
- * Request headers
- *
- * @var array
- */
- public $headers = array();
- /**
- * Request files (matches $_FILES)
- *
- * @var array
- */
- public $files = array();
- /**
- * Request/Response handler, either JSON by default
- * or XML if requested by client
- *
- * @var WC_API_Handler
- */
- public $handler;
- /**
- * Setup class and set request/response handler
- *
- * @since 2.1
- * @param $path
- */
- public function __construct( $path ) {
- if ( empty( $path ) ) {
- if ( isset( $_SERVER['PATH_INFO'] ) ) {
- $path = $_SERVER['PATH_INFO'];
- } else {
- $path = '/';
- }
- }
- $this->path = $path;
- $this->method = $_SERVER['REQUEST_METHOD'];
- $this->params['GET'] = $_GET;
- $this->params['POST'] = $_POST;
- $this->headers = $this->get_headers( $_SERVER );
- $this->files = $_FILES;
- // Compatibility for clients that can't use PUT/PATCH/DELETE
- if ( isset( $_GET['_method'] ) ) {
- $this->method = strtoupper( $_GET['_method'] );
- } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
- $this->method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
- }
- // load response handler
- $handler_class = apply_filters( 'woocommerce_api_default_response_handler', 'WC_API_JSON_Handler', $this->path, $this );
- $this->handler = new $handler_class();
- }
- /**
- * Check authentication for the request
- *
- * @since 2.1
- * @return WP_User|WP_Error WP_User object indicates successful login, WP_Error indicates unsuccessful login
- */
- public function check_authentication() {
- // allow plugins to remove default authentication or add their own authentication
- $user = apply_filters( 'woocommerce_api_check_authentication', null, $this );
- if ( is_a( $user, 'WP_User' ) ) {
- // API requests run under the context of the authenticated user
- wp_set_current_user( $user->ID );
- } elseif ( ! is_wp_error( $user ) ) {
- // WP_Errors are handled in serve_request()
- $user = new WP_Error( 'woocommerce_api_authentication_error', __( 'Invalid authentication method', 'woocommerce' ), array( 'code' => 500 ) );
- }
- return $user;
- }
- /**
- * Convert an error to an array
- *
- * This iterates over all error codes and messages to change it into a flat
- * array. This enables simpler client behaviour, as it is represented as a
- * list in JSON rather than an object/map
- *
- * @since 2.1
- * @param WP_Error $error
- * @return array List of associative arrays with code and message keys
- */
- protected function error_to_array( $error ) {
- $errors = array();
- foreach ( (array) $error->errors as $code => $messages ) {
- foreach ( (array) $messages as $message ) {
- $errors[] = array( 'code' => $code, 'message' => $message );
- }
- }
- return array( 'errors' => $errors );
- }
- /**
- * Handle serving an API request
- *
- * Matches the current server URI to a route and runs the first matching
- * callback then outputs a JSON representation of the returned value.
- *
- * @since 2.1
- * @uses WC_API_Server::dispatch()
- */
- public function serve_request() {
- do_action( 'woocommerce_api_server_before_serve', $this );
- $this->header( 'Content-Type', $this->handler->get_content_type(), true );
- // the API is enabled by default
- if ( ! apply_filters( 'woocommerce_api_enabled', true, $this ) || ( 'no' === get_option( 'woocommerce_api_enabled' ) ) ) {
- $this->send_status( 404 );
- echo $this->handler->generate_response( array( 'errors' => array( 'code' => 'woocommerce_api_disabled', 'message' => 'The WooCommerce API is disabled on this site' ) ) );
- return;
- }
- $result = $this->check_authentication();
- // if authorization check was successful, dispatch the request
- if ( ! is_wp_error( $result ) ) {
- $result = $this->dispatch();
- }
- // handle any dispatch errors
- if ( is_wp_error( $result ) ) {
- $data = $result->get_error_data();
- if ( is_array( $data ) && isset( $data['status'] ) ) {
- $this->send_status( $data['status'] );
- }
- $result = $this->error_to_array( $result );
- }
- // This is a filter rather than an action, since this is designed to be
- // re-entrant if needed
- $served = apply_filters( 'woocommerce_api_serve_request', false, $result, $this );
- if ( ! $served ) {
- if ( 'HEAD' === $this->method ) {
- return;
- }
- echo $this->handler->generate_response( $result );
- }
- }
- /**
- * Retrieve the route map
- *
- * The route map is an associative array with path regexes as the keys. The
- * value is an indexed array with the callback function/method as the first
- * item, and a bitmask of HTTP methods as the second item (see the class
- * constants).
- *
- * Each route can be mapped to more than one callback by using an array of
- * the indexed arrays. This allows mapping e.g. GET requests to one callback
- * and POST requests to another.
- *
- * Note that the path regexes (array keys) must have @ escaped, as this is
- * used as the delimiter with preg_match()
- *
- * @since 2.1
- * @return array `'/path/regex' => array( $callback, $bitmask )` or `'/path/regex' => array( array( $callback, $bitmask ), ...)`
- */
- public function get_routes() {
- // index added by default
- $endpoints = array(
- '/' => array( array( $this, 'get_index' ), self::READABLE ),
- );
- $endpoints = apply_filters( 'woocommerce_api_endpoints', $endpoints );
- // Normalise the endpoints
- foreach ( $endpoints as $route => &$handlers ) {
- if ( count( $handlers ) <= 2 && isset( $handlers[1] ) && ! is_array( $handlers[1] ) ) {
- $handlers = array( $handlers );
- }
- }
- return $endpoints;
- }
- /**
- * Match the request to a callback and call it
- *
- * @since 2.1
- * @return mixed The value returned by the callback, or a WP_Error instance
- */
- public function dispatch() {
- switch ( $this->method ) {
- case 'HEAD' :
- case 'GET' :
- $method = self::METHOD_GET;
- break;
- case 'POST' :
- $method = self::METHOD_POST;
- break;
- case 'PUT' :
- $method = self::METHOD_PUT;
- break;
- case 'PATCH' :
- $method = self::METHOD_PATCH;
- break;
- case 'DELETE' :
- $method = self::METHOD_DELETE;
- break;
- default :
- return new WP_Error( 'woocommerce_api_unsupported_method', __( 'Unsupported request method', 'woocommerce' ), array( 'status' => 400 ) );
- }
- foreach ( $this->get_routes() as $route => $handlers ) {
- foreach ( $handlers as $handler ) {
- $callback = $handler[0];
- $supported = isset( $handler[1] ) ? $handler[1] : self::METHOD_GET;
- if ( ! ( $supported & $method ) ) {
- continue;
- }
- $match = preg_match( '@^' . $route . '$@i', urldecode( $this->path ), $args );
- if ( ! $match ) {
- continue;
- }
- if ( ! is_callable( $callback ) ) {
- return new WP_Error( 'woocommerce_api_invalid_handler', __( 'The handler for the route is invalid', 'woocommerce' ), array( 'status' => 500 ) );
- }
- $args = array_merge( $args, $this->params['GET'] );
- if ( $method & self::METHOD_POST ) {
- $args = array_merge( $args, $this->params['POST'] );
- }
- if ( $supported & self::ACCEPT_DATA ) {
- $data = $this->handler->parse_body( $this->get_raw_data() );
- $args = array_merge( $args, array( 'data' => $data ) );
- } elseif ( $supported & self::ACCEPT_RAW_DATA ) {
- $data = $this->get_raw_data();
- $args = array_merge( $args, array( 'data' => $data ) );
- }
- $args['_method'] = $method;
- $args['_route'] = $route;
- $args['_path'] = $this->path;
- $args['_headers'] = $this->headers;
- $args['_files'] = $this->files;
- $args = apply_filters( 'woocommerce_api_dispatch_args', $args, $callback );
- // Allow plugins to halt the request via this filter
- if ( is_wp_error( $args ) ) {
- return $args;
- }
- $params = $this->sort_callback_params( $callback, $args );
- if ( is_wp_error( $params ) ) {
- return $params;
- }
- return call_user_func_array( $callback, $params );
- }
- }
- return new WP_Error( 'woocommerce_api_no_route', __( 'No route was found matching the URL and request method', 'woocommerce' ), array( 'status' => 404 ) );
- }
- /**
- * urldecode deep.
- *
- * @since 2.2
- * @param string|array $value Data to decode with urldecode.
- * @return string|array Decoded data.
- */
- protected function urldecode_deep( $value ) {
- if ( is_array( $value ) ) {
- return array_map( array( $this, 'urldecode_deep' ), $value );
- } else {
- return urldecode( $value );
- }
- }
- /**
- * Sort parameters by order specified in method declaration
- *
- * Takes a callback and a list of available params, then filters and sorts
- * by the parameters the method actually needs, using the Reflection API
- *
- * @since 2.2
- *
- * @param callable|array $callback the endpoint callback
- * @param array $provided the provided request parameters
- *
- * @return array|WP_Error
- */
- protected function sort_callback_params( $callback, $provided ) {
- if ( is_array( $callback ) ) {
- $ref_func = new ReflectionMethod( $callback[0], $callback[1] );
- } else {
- $ref_func = new ReflectionFunction( $callback );
- }
- $wanted = $ref_func->getParameters();
- $ordered_parameters = array();
- foreach ( $wanted as $param ) {
- if ( isset( $provided[ $param->getName() ] ) ) {
- // We have this parameters in the list to choose from
- if ( 'data' == $param->getName() ) {
- $ordered_parameters[] = $provided[ $param->getName() ];
- continue;
- }
- $ordered_parameters[] = $this->urldecode_deep( $provided[ $param->getName() ] );
- } elseif ( $param->isDefaultValueAvailable() ) {
- // We don't have this parameter, but it's optional
- $ordered_parameters[] = $param->getDefaultValue();
- } else {
- // We don't have this parameter and it wasn't optional, abort!
- return new WP_Error( 'woocommerce_api_missing_callback_param', sprintf( __( 'Missing parameter %s', 'woocommerce' ), $param->getName() ), array( 'status' => 400 ) );
- }
- }
- return $ordered_parameters;
- }
- /**
- * Get the site index.
- *
- * This endpoint describes the capabilities of the site.
- *
- * @since 2.3
- * @return array Index entity
- */
- public function get_index() {
- // General site data
- $available = array(
- 'store' => array(
- 'name' => get_option( 'blogname' ),
- 'description' => get_option( 'blogdescription' ),
- 'URL' => get_option( 'siteurl' ),
- 'wc_version' => WC()->version,
- 'routes' => array(),
- 'meta' => array(
- 'timezone' => wc_timezone_string(),
- 'currency' => get_woocommerce_currency(),
- 'currency_format' => get_woocommerce_currency_symbol(),
- 'currency_position' => get_option( 'woocommerce_currency_pos' ),
- 'thousand_separator' => get_option( 'woocommerce_price_thousand_sep' ),
- 'decimal_separator' => get_option( 'woocommerce_price_decimal_sep' ),
- 'price_num_decimals' => wc_get_price_decimals(),
- 'tax_included' => wc_prices_include_tax(),
- 'weight_unit' => get_option( 'woocommerce_weight_unit' ),
- 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ),
- 'ssl_enabled' => ( 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ),
- 'permalinks_enabled' => ( '' !== get_option( 'permalink_structure' ) ),
- 'generate_password' => ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ),
- 'links' => array(
- 'help' => 'https://woocommerce.github.io/woocommerce-rest-api-docs/',
- ),
- ),
- ),
- );
- // Find the available routes
- foreach ( $this->get_routes() as $route => $callbacks ) {
- $data = array();
- $route = preg_replace( '#\(\?P(<\w+?>).*?\)#', '$1', $route );
- foreach ( self::$method_map as $name => $bitmask ) {
- foreach ( $callbacks as $callback ) {
- // Skip to the next route if any callback is hidden
- if ( $callback[1] & self::HIDDEN_ENDPOINT ) {
- continue 3;
- }
- if ( $callback[1] & $bitmask ) {
- $data['supports'][] = $name;
- }
- if ( $callback[1] & self::ACCEPT_DATA ) {
- $data['accepts_data'] = true;
- }
- // For non-variable routes, generate links
- if ( strpos( $route, '<' ) === false ) {
- $data['meta'] = array(
- 'self' => get_woocommerce_api_url( $route ),
- );
- }
- }
- }
- $available['store']['routes'][ $route ] = apply_filters( 'woocommerce_api_endpoints_description', $data );
- }
- return apply_filters( 'woocommerce_api_index', $available );
- }
- /**
- * Send a HTTP status code
- *
- * @since 2.1
- * @param int $code HTTP status
- */
- public function send_status( $code ) {
- status_header( $code );
- }
- /**
- * Send a HTTP header
- *
- * @since 2.1
- * @param string $key Header key
- * @param string $value Header value
- * @param boolean $replace Should we replace the existing header?
- */
- public function header( $key, $value, $replace = true ) {
- header( sprintf( '%s: %s', $key, $value ), $replace );
- }
- /**
- * Send a Link header
- *
- * @internal The $rel parameter is first, as this looks nicer when sending multiple
- *
- * @link http://tools.ietf.org/html/rfc5988
- * @link http://www.iana.org/assignments/link-relations/link-relations.xml
- *
- * @since 2.1
- * @param string $rel Link relation. Either a registered type, or an absolute URL
- * @param string $link Target IRI for the link
- * @param array $other Other parameters to send, as an associative array
- */
- public function link_header( $rel, $link, $other = array() ) {
- $header = sprintf( '<%s>; rel="%s"', $link, esc_attr( $rel ) );
- foreach ( $other as $key => $value ) {
- if ( 'title' == $key ) {
- $value = '"' . $value . '"';
- }
- $header .= '; ' . $key . '=' . $value;
- }
- $this->header( 'Link', $header, false );
- }
- /**
- * Send pagination headers for resources
- *
- * @since 2.1
- * @param WP_Query|WP_User_Query|stdClass $query
- */
- public function add_pagination_headers( $query ) {
- // WP_User_Query
- if ( is_a( $query, 'WP_User_Query' ) ) {
- $single = count( $query->get_results() ) == 1;
- $total = $query->get_total();
- if ( $query->get( 'number' ) > 0 ) {
- $page = ( $query->get( 'offset' ) / $query->get( 'number' ) ) + 1;
- $total_pages = ceil( $total / $query->get( 'number' ) );
- } else {
- $page = 1;
- $total_pages = 1;
- }
- } elseif ( is_a( $query, 'stdClass' ) ) {
- $page = $query->page;
- $single = $query->is_single;
- $total = $query->total;
- $total_pages = $query->total_pages;
- // WP_Query
- } else {
- $page = $query->get( 'paged' );
- $single = $query->is_single();
- $total = $query->found_posts;
- $total_pages = $query->max_num_pages;
- }
- if ( ! $page ) {
- $page = 1;
- }
- $next_page = absint( $page ) + 1;
- if ( ! $single ) {
- // first/prev
- if ( $page > 1 ) {
- $this->link_header( 'first', $this->get_paginated_url( 1 ) );
- $this->link_header( 'prev', $this->get_paginated_url( $page -1 ) );
- }
- // next
- if ( $next_page <= $total_pages ) {
- $this->link_header( 'next', $this->get_paginated_url( $next_page ) );
- }
- // last
- if ( $page != $total_pages ) {
- $this->link_header( 'last', $this->get_paginated_url( $total_pages ) );
- }
- }
- $this->header( 'X-WC-Total', $total );
- $this->header( 'X-WC-TotalPages', $total_pages );
- do_action( 'woocommerce_api_pagination_headers', $this, $query );
- }
- /**
- * Returns the request URL with the page query parameter set to the specified page
- *
- * @since 2.1
- * @param int $page
- * @return string
- */
- private function get_paginated_url( $page ) {
- // remove existing page query param
- $request = remove_query_arg( 'page' );
- // add provided page query param
- $request = urldecode( add_query_arg( 'page', $page, $request ) );
- // get the home host
- $host = parse_url( get_home_url(), PHP_URL_HOST );
- return set_url_scheme( "http://{$host}{$request}" );
- }
- /**
- * Retrieve the raw request entity (body)
- *
- * @since 2.1
- * @return string
- */
- public function get_raw_data() {
- // @codingStandardsIgnoreStart
- // $HTTP_RAW_POST_DATA is deprecated on PHP 5.6.
- if ( function_exists( 'phpversion' ) && version_compare( phpversion(), '5.6', '>=' ) ) {
- return file_get_contents( 'php://input' );
- }
- global $HTTP_RAW_POST_DATA;
- // A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
- // but we can do it ourself.
- if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
- $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
- }
- return $HTTP_RAW_POST_DATA;
- // @codingStandardsIgnoreEnd
- }
- /**
- * Parse an RFC3339 datetime into a MySQl datetime
- *
- * Invalid dates default to unix epoch
- *
- * @since 2.1
- * @param string $datetime RFC3339 datetime
- * @return string MySQl datetime (YYYY-MM-DD HH:MM:SS)
- */
- public function parse_datetime( $datetime ) {
- // Strip millisecond precision (a full stop followed by one or more digits)
- if ( strpos( $datetime, '.' ) !== false ) {
- $datetime = preg_replace( '/\.\d+/', '', $datetime );
- }
- // default timezone to UTC
- $datetime = preg_replace( '/[+-]\d+:+\d+$/', '+00:00', $datetime );
- try {
- $datetime = new DateTime( $datetime, new DateTimeZone( 'UTC' ) );
- } catch ( Exception $e ) {
- $datetime = new DateTime( '@0' );
- }
- return $datetime->format( 'Y-m-d H:i:s' );
- }
- /**
- * Format a unix timestamp or MySQL datetime into an RFC3339 datetime
- *
- * @since 2.1
- * @param int|string $timestamp unix timestamp or MySQL datetime
- * @param bool $convert_to_utc
- * @param bool $convert_to_gmt Use GMT timezone.
- * @return string RFC3339 datetime
- */
- public function format_datetime( $timestamp, $convert_to_utc = false, $convert_to_gmt = false ) {
- if ( $convert_to_gmt ) {
- if ( is_numeric( $timestamp ) ) {
- $timestamp = date( 'Y-m-d H:i:s', $timestamp );
- }
- $timestamp = get_gmt_from_date( $timestamp );
- }
- if ( $convert_to_utc ) {
- $timezone = new DateTimeZone( wc_timezone_string() );
- } else {
- $timezone = new DateTimeZone( 'UTC' );
- }
- try {
- if ( is_numeric( $timestamp ) ) {
- $date = new DateTime( "@{$timestamp}" );
- } else {
- $date = new DateTime( $timestamp, $timezone );
- }
- // convert to UTC by adjusting the time based on the offset of the site's timezone
- if ( $convert_to_utc ) {
- $date->modify( -1 * $date->getOffset() . ' seconds' );
- }
- } catch ( Exception $e ) {
- $date = new DateTime( '@0' );
- }
- return $date->format( 'Y-m-d\TH:i:s\Z' );
- }
- /**
- * Extract headers from a PHP-style $_SERVER array
- *
- * @since 2.1
- * @param array $server Associative array similar to $_SERVER
- * @return array Headers extracted from the input
- */
- public function get_headers( $server ) {
- $headers = array();
- // CONTENT_* headers are not prefixed with HTTP_
- $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
- foreach ( $server as $key => $value ) {
- if ( strpos( $key, 'HTTP_' ) === 0 ) {
- $headers[ substr( $key, 5 ) ] = $value;
- } elseif ( isset( $additional[ $key ] ) ) {
- $headers[ $key ] = $value;
- }
- }
- return $headers;
- }
- }
|