File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/rest-api.tar
class-wp-rest-request.php 0000644 00000063027 15153067731 0011467 0 ustar 00 <?php
/**
* REST API: WP_REST_Request class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement a REST request object.
*
* Contains data from the request, to be passed to the callback.
*
* Note: This implements ArrayAccess, and acts as an array of parameters when
* used in that manner. It does not use ArrayObject (as we cannot rely on SPL),
* so be aware it may have non-array behavior in some cases.
*
* Note: When using features provided by ArrayAccess, be aware that WordPress deliberately
* does not distinguish between arguments of the same name for different request methods.
* For instance, in a request with `GET id=1` and `POST id=2`, `$request['id']` will equal
* 2 (`POST`) not 1 (`GET`). For more precision between request methods, use
* WP_REST_Request::get_body_params(), WP_REST_Request::get_url_params(), etc.
*
* @since 4.4.0
*
* @link https://www.php.net/manual/en/class.arrayaccess.php
*/
#[AllowDynamicProperties]
class WP_REST_Request implements ArrayAccess {
/**
* HTTP method.
*
* @since 4.4.0
* @var string
*/
protected $method = '';
/**
* Parameters passed to the request.
*
* These typically come from the `$_GET`, `$_POST` and `$_FILES`
* superglobals when being created from the global scope.
*
* @since 4.4.0
* @var array Contains GET, POST and FILES keys mapping to arrays of data.
*/
protected $params;
/**
* HTTP headers for the request.
*
* @since 4.4.0
* @var array Map of key to value. Key is always lowercase, as per HTTP specification.
*/
protected $headers = array();
/**
* Body data.
*
* @since 4.4.0
* @var string Binary data from the request.
*/
protected $body = null;
/**
* Route matched for the request.
*
* @since 4.4.0
* @var string
*/
protected $route;
/**
* Attributes (options) for the route that was matched.
*
* This is the options array used when the route was registered, typically
* containing the callback as well as the valid methods for the route.
*
* @since 4.4.0
* @var array Attributes for the request.
*/
protected $attributes = array();
/**
* Used to determine if the JSON data has been parsed yet.
*
* Allows lazy-parsing of JSON data where possible.
*
* @since 4.4.0
* @var bool
*/
protected $parsed_json = false;
/**
* Used to determine if the body data has been parsed yet.
*
* @since 4.4.0
* @var bool
*/
protected $parsed_body = false;
/**
* Constructor.
*
* @since 4.4.0
*
* @param string $method Optional. Request method. Default empty.
* @param string $route Optional. Request route. Default empty.
* @param array $attributes Optional. Request attributes. Default empty array.
*/
public function __construct( $method = '', $route = '', $attributes = array() ) {
$this->params = array(
'URL' => array(),
'GET' => array(),
'POST' => array(),
'FILES' => array(),
// See parse_json_params.
'JSON' => null,
'defaults' => array(),
);
$this->set_method( $method );
$this->set_route( $route );
$this->set_attributes( $attributes );
}
/**
* Retrieves the HTTP method for the request.
*
* @since 4.4.0
*
* @return string HTTP method.
*/
public function get_method() {
return $this->method;
}
/**
* Sets HTTP method for the request.
*
* @since 4.4.0
*
* @param string $method HTTP method.
*/
public function set_method( $method ) {
$this->method = strtoupper( $method );
}
/**
* Retrieves all headers from the request.
*
* @since 4.4.0
*
* @return array Map of key to value. Key is always lowercase, as per HTTP specification.
*/
public function get_headers() {
return $this->headers;
}
/**
* Canonicalizes the header name.
*
* Ensures that header names are always treated the same regardless of
* source. Header names are always case insensitive.
*
* Note that we treat `-` (dashes) and `_` (underscores) as the same
* character, as per header parsing rules in both Apache and nginx.
*
* @link https://stackoverflow.com/q/18185366
* @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
* @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
*
* @since 4.4.0
*
* @param string $key Header name.
* @return string Canonicalized name.
*/
public static function canonicalize_header_name( $key ) {
$key = strtolower( $key );
$key = str_replace( '-', '_', $key );
return $key;
}
/**
* Retrieves the given header from the request.
*
* If the header has multiple values, they will be concatenated with a comma
* as per the HTTP specification. Be aware that some non-compliant headers
* (notably cookie headers) cannot be joined this way.
*
* @since 4.4.0
*
* @param string $key Header name, will be canonicalized to lowercase.
* @return string|null String value if set, null otherwise.
*/
public function get_header( $key ) {
$key = $this->canonicalize_header_name( $key );
if ( ! isset( $this->headers[ $key ] ) ) {
return null;
}
return implode( ',', $this->headers[ $key ] );
}
/**
* Retrieves header values from the request.
*
* @since 4.4.0
*
* @param string $key Header name, will be canonicalized to lowercase.
* @return array|null List of string values if set, null otherwise.
*/
public function get_header_as_array( $key ) {
$key = $this->canonicalize_header_name( $key );
if ( ! isset( $this->headers[ $key ] ) ) {
return null;
}
return $this->headers[ $key ];
}
/**
* Sets the header on request.
*
* @since 4.4.0
*
* @param string $key Header name.
* @param string $value Header value, or list of values.
*/
public function set_header( $key, $value ) {
$key = $this->canonicalize_header_name( $key );
$value = (array) $value;
$this->headers[ $key ] = $value;
}
/**
* Appends a header value for the given header.
*
* @since 4.4.0
*
* @param string $key Header name.
* @param string $value Header value, or list of values.
*/
public function add_header( $key, $value ) {
$key = $this->canonicalize_header_name( $key );
$value = (array) $value;
if ( ! isset( $this->headers[ $key ] ) ) {
$this->headers[ $key ] = array();
}
$this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
}
/**
* Removes all values for a header.
*
* @since 4.4.0
*
* @param string $key Header name.
*/
public function remove_header( $key ) {
$key = $this->canonicalize_header_name( $key );
unset( $this->headers[ $key ] );
}
/**
* Sets headers on the request.
*
* @since 4.4.0
*
* @param array $headers Map of header name to value.
* @param bool $override If true, replace the request's headers. Otherwise, merge with existing.
*/
public function set_headers( $headers, $override = true ) {
if ( true === $override ) {
$this->headers = array();
}
foreach ( $headers as $key => $value ) {
$this->set_header( $key, $value );
}
}
/**
* Retrieves the Content-Type of the request.
*
* @since 4.4.0
*
* @return array|null Map containing 'value' and 'parameters' keys
* or null when no valid Content-Type header was
* available.
*/
public function get_content_type() {
$value = $this->get_header( 'Content-Type' );
if ( empty( $value ) ) {
return null;
}
$parameters = '';
if ( strpos( $value, ';' ) ) {
list( $value, $parameters ) = explode( ';', $value, 2 );
}
$value = strtolower( $value );
if ( ! str_contains( $value, '/' ) ) {
return null;
}
// Parse type and subtype out.
list( $type, $subtype ) = explode( '/', $value, 2 );
$data = compact( 'value', 'type', 'subtype', 'parameters' );
$data = array_map( 'trim', $data );
return $data;
}
/**
* Checks if the request has specified a JSON Content-Type.
*
* @since 5.6.0
*
* @return bool True if the Content-Type header is JSON.
*/
public function is_json_content_type() {
$content_type = $this->get_content_type();
return isset( $content_type['value'] ) && wp_is_json_media_type( $content_type['value'] );
}
/**
* Retrieves the parameter priority order.
*
* Used when checking parameters in WP_REST_Request::get_param().
*
* @since 4.4.0
*
* @return string[] Array of types to check, in order of priority.
*/
protected function get_parameter_order() {
$order = array();
if ( $this->is_json_content_type() ) {
$order[] = 'JSON';
}
$this->parse_json_params();
// Ensure we parse the body data.
$body = $this->get_body();
if ( 'POST' !== $this->method && ! empty( $body ) ) {
$this->parse_body_params();
}
$accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
if ( in_array( $this->method, $accepts_body_data, true ) ) {
$order[] = 'POST';
}
$order[] = 'GET';
$order[] = 'URL';
$order[] = 'defaults';
/**
* Filters the parameter priority order for a REST API request.
*
* The order affects which parameters are checked when using WP_REST_Request::get_param()
* and family. This acts similarly to PHP's `request_order` setting.
*
* @since 4.4.0
*
* @param string[] $order Array of types to check, in order of priority.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_request_parameter_order', $order, $this );
}
/**
* Retrieves a parameter from the request.
*
* @since 4.4.0
*
* @param string $key Parameter name.
* @return mixed|null Value if set, null otherwise.
*/
public function get_param( $key ) {
$order = $this->get_parameter_order();
foreach ( $order as $type ) {
// Determine if we have the parameter for this type.
if ( isset( $this->params[ $type ][ $key ] ) ) {
return $this->params[ $type ][ $key ];
}
}
return null;
}
/**
* Checks if a parameter exists in the request.
*
* This allows distinguishing between an omitted parameter,
* and a parameter specifically set to null.
*
* @since 5.3.0
*
* @param string $key Parameter name.
* @return bool True if a param exists for the given key.
*/
public function has_param( $key ) {
$order = $this->get_parameter_order();
foreach ( $order as $type ) {
if ( is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
return true;
}
}
return false;
}
/**
* Sets a parameter on the request.
*
* If the given parameter key exists in any parameter type an update will take place,
* otherwise a new param will be created in the first parameter type (respecting
* get_parameter_order()).
*
* @since 4.4.0
*
* @param string $key Parameter name.
* @param mixed $value Parameter value.
*/
public function set_param( $key, $value ) {
$order = $this->get_parameter_order();
$found_key = false;
foreach ( $order as $type ) {
if ( 'defaults' !== $type && is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
$this->params[ $type ][ $key ] = $value;
$found_key = true;
}
}
if ( ! $found_key ) {
$this->params[ $order[0] ][ $key ] = $value;
}
}
/**
* Retrieves merged parameters from the request.
*
* The equivalent of get_param(), but returns all parameters for the request.
* Handles merging all the available values into a single array.
*
* @since 4.4.0
*
* @return array Map of key to value.
*/
public function get_params() {
$order = $this->get_parameter_order();
$order = array_reverse( $order, true );
$params = array();
foreach ( $order as $type ) {
/*
* array_merge() / the "+" operator will mess up
* numeric keys, so instead do a manual foreach.
*/
foreach ( (array) $this->params[ $type ] as $key => $value ) {
$params[ $key ] = $value;
}
}
return $params;
}
/**
* Retrieves parameters from the route itself.
*
* These are parsed from the URL using the regex.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_url_params() {
return $this->params['URL'];
}
/**
* Sets parameters from the route.
*
* Typically, this is set after parsing the URL.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_url_params( $params ) {
$this->params['URL'] = $params;
}
/**
* Retrieves parameters from the query string.
*
* These are the parameters you'd typically find in `$_GET`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_query_params() {
return $this->params['GET'];
}
/**
* Sets parameters from the query string.
*
* Typically, this is set from `$_GET`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_query_params( $params ) {
$this->params['GET'] = $params;
}
/**
* Retrieves parameters from the body.
*
* These are the parameters you'd typically find in `$_POST`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_body_params() {
return $this->params['POST'];
}
/**
* Sets parameters from the body.
*
* Typically, this is set from `$_POST`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_body_params( $params ) {
$this->params['POST'] = $params;
}
/**
* Retrieves multipart file parameters from the body.
*
* These are the parameters you'd typically find in `$_FILES`.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_file_params() {
return $this->params['FILES'];
}
/**
* Sets multipart file parameters from the body.
*
* Typically, this is set from `$_FILES`.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_file_params( $params ) {
$this->params['FILES'] = $params;
}
/**
* Retrieves the default parameters.
*
* These are the parameters set in the route registration.
*
* @since 4.4.0
*
* @return array Parameter map of key to value
*/
public function get_default_params() {
return $this->params['defaults'];
}
/**
* Sets default parameters.
*
* These are the parameters set in the route registration.
*
* @since 4.4.0
*
* @param array $params Parameter map of key to value.
*/
public function set_default_params( $params ) {
$this->params['defaults'] = $params;
}
/**
* Retrieves the request body content.
*
* @since 4.4.0
*
* @return string Binary data from the request body.
*/
public function get_body() {
return $this->body;
}
/**
* Sets body content.
*
* @since 4.4.0
*
* @param string $data Binary data from the request body.
*/
public function set_body( $data ) {
$this->body = $data;
// Enable lazy parsing.
$this->parsed_json = false;
$this->parsed_body = false;
$this->params['JSON'] = null;
}
/**
* Retrieves the parameters from a JSON-formatted body.
*
* @since 4.4.0
*
* @return array Parameter map of key to value.
*/
public function get_json_params() {
// Ensure the parameters have been parsed out.
$this->parse_json_params();
return $this->params['JSON'];
}
/**
* Parses the JSON parameters.
*
* Avoids parsing the JSON data until we need to access it.
*
* @since 4.4.0
* @since 4.7.0 Returns error instance if value cannot be decoded.
* @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
*/
protected function parse_json_params() {
if ( $this->parsed_json ) {
return true;
}
$this->parsed_json = true;
// Check that we actually got JSON.
if ( ! $this->is_json_content_type() ) {
return true;
}
$body = $this->get_body();
if ( empty( $body ) ) {
return true;
}
$params = json_decode( $body, true );
/*
* Check for a parsing error.
*/
if ( null === $params && JSON_ERROR_NONE !== json_last_error() ) {
// Ensure subsequent calls receive error instance.
$this->parsed_json = false;
$error_data = array(
'status' => WP_Http::BAD_REQUEST,
'json_error_code' => json_last_error(),
'json_error_message' => json_last_error_msg(),
);
return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
}
$this->params['JSON'] = $params;
return true;
}
/**
* Parses the request body parameters.
*
* Parses out URL-encoded bodies for request methods that aren't supported
* natively by PHP. In PHP 5.x, only POST has these parsed automatically.
*
* @since 4.4.0
*/
protected function parse_body_params() {
if ( $this->parsed_body ) {
return;
}
$this->parsed_body = true;
/*
* Check that we got URL-encoded. Treat a missing Content-Type as
* URL-encoded for maximum compatibility.
*/
$content_type = $this->get_content_type();
if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
return;
}
parse_str( $this->get_body(), $params );
/*
* Add to the POST parameters stored internally. If a user has already
* set these manually (via `set_body_params`), don't override them.
*/
$this->params['POST'] = array_merge( $params, $this->params['POST'] );
}
/**
* Retrieves the route that matched the request.
*
* @since 4.4.0
*
* @return string Route matching regex.
*/
public function get_route() {
return $this->route;
}
/**
* Sets the route that matched the request.
*
* @since 4.4.0
*
* @param string $route Route matching regex.
*/
public function set_route( $route ) {
$this->route = $route;
}
/**
* Retrieves the attributes for the request.
*
* These are the options for the route that was matched.
*
* @since 4.4.0
*
* @return array Attributes for the request.
*/
public function get_attributes() {
return $this->attributes;
}
/**
* Sets the attributes for the request.
*
* @since 4.4.0
*
* @param array $attributes Attributes for the request.
*/
public function set_attributes( $attributes ) {
$this->attributes = $attributes;
}
/**
* Sanitizes (where possible) the params on the request.
*
* This is primarily based off the sanitize_callback param on each registered
* argument.
*
* @since 4.4.0
*
* @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
*/
public function sanitize_params() {
$attributes = $this->get_attributes();
// No arguments set, skip sanitizing.
if ( empty( $attributes['args'] ) ) {
return true;
}
$order = $this->get_parameter_order();
$invalid_params = array();
$invalid_details = array();
foreach ( $order as $type ) {
if ( empty( $this->params[ $type ] ) ) {
continue;
}
foreach ( $this->params[ $type ] as $key => $value ) {
if ( ! isset( $attributes['args'][ $key ] ) ) {
continue;
}
$param_args = $attributes['args'][ $key ];
// If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
$param_args['sanitize_callback'] = 'rest_parse_request_arg';
}
// If there's still no sanitize_callback, nothing to do here.
if ( empty( $param_args['sanitize_callback'] ) ) {
continue;
}
/** @var mixed|WP_Error $sanitized_value */
$sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );
if ( is_wp_error( $sanitized_value ) ) {
$invalid_params[ $key ] = implode( ' ', $sanitized_value->get_error_messages() );
$invalid_details[ $key ] = rest_convert_error_to_response( $sanitized_value )->get_data();
} else {
$this->params[ $type ][ $key ] = $sanitized_value;
}
}
}
if ( $invalid_params ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: List of invalid parameters. */
sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
array(
'status' => 400,
'params' => $invalid_params,
'details' => $invalid_details,
)
);
}
return true;
}
/**
* Checks whether this request is valid according to its attributes.
*
* @since 4.4.0
*
* @return true|WP_Error True if there are no parameters to validate or if all pass validation,
* WP_Error if required parameters are missing.
*/
public function has_valid_params() {
// If JSON data was passed, check for errors.
$json_error = $this->parse_json_params();
if ( is_wp_error( $json_error ) ) {
return $json_error;
}
$attributes = $this->get_attributes();
$required = array();
$args = empty( $attributes['args'] ) ? array() : $attributes['args'];
foreach ( $args as $key => $arg ) {
$param = $this->get_param( $key );
if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
$required[] = $key;
}
}
if ( ! empty( $required ) ) {
return new WP_Error(
'rest_missing_callback_param',
/* translators: %s: List of required parameters. */
sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ),
array(
'status' => 400,
'params' => $required,
)
);
}
/*
* Check the validation callbacks for each registered arg.
*
* This is done after required checking as required checking is cheaper.
*/
$invalid_params = array();
$invalid_details = array();
foreach ( $args as $key => $arg ) {
$param = $this->get_param( $key );
if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
/** @var bool|\WP_Error $valid_check */
$valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
if ( false === $valid_check ) {
$invalid_params[ $key ] = __( 'Invalid parameter.' );
}
if ( is_wp_error( $valid_check ) ) {
$invalid_params[ $key ] = implode( ' ', $valid_check->get_error_messages() );
$invalid_details[ $key ] = rest_convert_error_to_response( $valid_check )->get_data();
}
}
}
if ( $invalid_params ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: List of invalid parameters. */
sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
array(
'status' => 400,
'params' => $invalid_params,
'details' => $invalid_details,
)
);
}
if ( isset( $attributes['validate_callback'] ) ) {
$valid_check = call_user_func( $attributes['validate_callback'], $this );
if ( is_wp_error( $valid_check ) ) {
return $valid_check;
}
if ( false === $valid_check ) {
// A WP_Error instance is preferred, but false is supported for parity with the per-arg validate_callback.
return new WP_Error( 'rest_invalid_params', __( 'Invalid parameters.' ), array( 'status' => 400 ) );
}
}
return true;
}
/**
* Checks if a parameter is set.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @return bool Whether the parameter is set.
*/
#[ReturnTypeWillChange]
public function offsetExists( $offset ) {
$order = $this->get_parameter_order();
foreach ( $order as $type ) {
if ( isset( $this->params[ $type ][ $offset ] ) ) {
return true;
}
}
return false;
}
/**
* Retrieves a parameter from the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @return mixed|null Value if set, null otherwise.
*/
#[ReturnTypeWillChange]
public function offsetGet( $offset ) {
return $this->get_param( $offset );
}
/**
* Sets a parameter on the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
* @param mixed $value Parameter value.
*/
#[ReturnTypeWillChange]
public function offsetSet( $offset, $value ) {
$this->set_param( $offset, $value );
}
/**
* Removes a parameter from the request.
*
* @since 4.4.0
*
* @param string $offset Parameter name.
*/
#[ReturnTypeWillChange]
public function offsetUnset( $offset ) {
$order = $this->get_parameter_order();
// Remove the offset from every group.
foreach ( $order as $type ) {
unset( $this->params[ $type ][ $offset ] );
}
}
/**
* Retrieves a WP_REST_Request object from a full URL.
*
* @since 4.5.0
*
* @param string $url URL with protocol, domain, path and query args.
* @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
*/
public static function from_url( $url ) {
$bits = parse_url( $url );
$query_params = array();
if ( ! empty( $bits['query'] ) ) {
wp_parse_str( $bits['query'], $query_params );
}
$api_root = rest_url();
if ( get_option( 'permalink_structure' ) && str_starts_with( $url, $api_root ) ) {
// Pretty permalinks on, and URL is under the API root.
$api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
$route = parse_url( $api_url_part, PHP_URL_PATH );
} elseif ( ! empty( $query_params['rest_route'] ) ) {
// ?rest_route=... set directly.
$route = $query_params['rest_route'];
unset( $query_params['rest_route'] );
}
$request = false;
if ( ! empty( $route ) ) {
$request = new WP_REST_Request( 'GET', $route );
$request->set_query_params( $query_params );
}
/**
* Filters the REST API request generated from a URL.
*
* @since 4.5.0
*
* @param WP_REST_Request|false $request Generated request object, or false if URL
* could not be parsed.
* @param string $url URL the request was generated from.
*/
return apply_filters( 'rest_request_from_url', $request, $url );
}
}
class-wp-rest-response.php 0000644 00000016276 15153067731 0011641 0 ustar 00 <?php
/**
* REST API: WP_REST_Response class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement a REST response object.
*
* @since 4.4.0
*
* @see WP_HTTP_Response
*/
class WP_REST_Response extends WP_HTTP_Response {
/**
* Links related to the response.
*
* @since 4.4.0
* @var array
*/
protected $links = array();
/**
* The route that was to create the response.
*
* @since 4.4.0
* @var string
*/
protected $matched_route = '';
/**
* The handler that was used to create the response.
*
* @since 4.4.0
* @var null|array
*/
protected $matched_handler = null;
/**
* Adds a link to the response.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
*
* @since 4.4.0
*
* @link https://tools.ietf.org/html/rfc5988
* @link https://www.iana.org/assignments/link-relations/link-relations.xml
*
* @param string $rel Link relation. Either an IANA registered type,
* or an absolute URL.
* @param string $href Target URI for the link.
* @param array $attributes Optional. Link parameters to send along with the URL. Default empty array.
*/
public function add_link( $rel, $href, $attributes = array() ) {
if ( empty( $this->links[ $rel ] ) ) {
$this->links[ $rel ] = array();
}
if ( isset( $attributes['href'] ) ) {
// Remove the href attribute, as it's used for the main URL.
unset( $attributes['href'] );
}
$this->links[ $rel ][] = array(
'href' => $href,
'attributes' => $attributes,
);
}
/**
* Removes a link from the response.
*
* @since 4.4.0
*
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string $href Optional. Only remove links for the relation matching the given href.
* Default null.
*/
public function remove_link( $rel, $href = null ) {
if ( ! isset( $this->links[ $rel ] ) ) {
return;
}
if ( $href ) {
$this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' );
} else {
$this->links[ $rel ] = array();
}
if ( ! $this->links[ $rel ] ) {
unset( $this->links[ $rel ] );
}
}
/**
* Adds multiple links to the response.
*
* Link data should be an associative array with link relation as the key.
* The value can either be an associative array of link attributes
* (including `href` with the URL for the response), or a list of these
* associative arrays.
*
* @since 4.4.0
*
* @param array $links Map of link relation to list of links.
*/
public function add_links( $links ) {
foreach ( $links as $rel => $set ) {
// If it's a single link, wrap with an array for consistent handling.
if ( isset( $set['href'] ) ) {
$set = array( $set );
}
foreach ( $set as $attributes ) {
$this->add_link( $rel, $attributes['href'], $attributes );
}
}
}
/**
* Retrieves links for the response.
*
* @since 4.4.0
*
* @return array List of links.
*/
public function get_links() {
return $this->links;
}
/**
* Sets a single link header.
*
* @internal The $rel parameter is first, as this looks nicer when sending multiple.
*
* @since 4.4.0
*
* @link https://tools.ietf.org/html/rfc5988
* @link https://www.iana.org/assignments/link-relations/link-relations.xml
*
* @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
* @param string $link Target IRI for the link.
* @param array $other Optional. Other parameters to send, as an associative array.
* Default empty array.
*/
public function link_header( $rel, $link, $other = array() ) {
$header = '<' . $link . '>; rel="' . $rel . '"';
foreach ( $other as $key => $value ) {
if ( 'title' === $key ) {
$value = '"' . $value . '"';
}
$header .= '; ' . $key . '=' . $value;
}
$this->header( 'Link', $header, false );
}
/**
* Retrieves the route that was used.
*
* @since 4.4.0
*
* @return string The matched route.
*/
public function get_matched_route() {
return $this->matched_route;
}
/**
* Sets the route (regex for path) that caused the response.
*
* @since 4.4.0
*
* @param string $route Route name.
*/
public function set_matched_route( $route ) {
$this->matched_route = $route;
}
/**
* Retrieves the handler that was used to generate the response.
*
* @since 4.4.0
*
* @return null|array The handler that was used to create the response.
*/
public function get_matched_handler() {
return $this->matched_handler;
}
/**
* Sets the handler that was responsible for generating the response.
*
* @since 4.4.0
*
* @param array $handler The matched handler.
*/
public function set_matched_handler( $handler ) {
$this->matched_handler = $handler;
}
/**
* Checks if the response is an error, i.e. >= 400 response code.
*
* @since 4.4.0
*
* @return bool Whether the response is an error.
*/
public function is_error() {
return $this->get_status() >= 400;
}
/**
* Retrieves a WP_Error object from the response.
*
* @since 4.4.0
*
* @return WP_Error|null WP_Error or null on not an errored response.
*/
public function as_error() {
if ( ! $this->is_error() ) {
return null;
}
$error = new WP_Error();
if ( is_array( $this->get_data() ) ) {
$data = $this->get_data();
$error->add( $data['code'], $data['message'], $data['data'] );
if ( ! empty( $data['additional_errors'] ) ) {
foreach ( $data['additional_errors'] as $err ) {
$error->add( $err['code'], $err['message'], $err['data'] );
}
}
} else {
$error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) );
}
return $error;
}
/**
* Retrieves the CURIEs (compact URIs) used for relations.
*
* @since 4.5.0
*
* @return array Compact URIs.
*/
public function get_curies() {
$curies = array(
array(
'name' => 'wp',
'href' => 'https://api.w.org/{rel}',
'templated' => true,
),
);
/**
* Filters extra CURIEs available on REST API responses.
*
* CURIEs allow a shortened version of URI relations. This allows a more
* usable form for custom relations than using the full URI. These work
* similarly to how XML namespaces work.
*
* Registered CURIES need to specify a name and URI template. This will
* automatically transform URI relations into their shortened version.
* The shortened relation follows the format `{name}:{rel}`. `{rel}` in
* the URI template will be replaced with the `{rel}` part of the
* shortened relation.
*
* For example, a CURIE with name `example` and URI template
* `http://w.org/{rel}` would transform a `http://w.org/term` relation
* into `example:term`.
*
* Well-behaved clients should expand and normalize these back to their
* full URI relation, however some naive clients may not resolve these
* correctly, so adding new CURIEs may break backward compatibility.
*
* @since 4.5.0
*
* @param array $additional Additional CURIEs to register with the REST API.
*/
$additional = apply_filters( 'rest_response_link_curies', array() );
return array_merge( $curies, $additional );
}
}
class-wp-rest-server.php 0000644 00000155436 15153067731 0011313 0 ustar 00 <?php
/**
* REST API: WP_REST_Server class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement the WordPress REST API server.
*
* @since 4.4.0
*/
#[AllowDynamicProperties]
class WP_REST_Server {
/**
* Alias for GET transport method.
*
* @since 4.4.0
* @var string
*/
const READABLE = 'GET';
/**
* Alias for POST transport method.
*
* @since 4.4.0
* @var string
*/
const CREATABLE = 'POST';
/**
* Alias for POST, PUT, PATCH transport methods together.
*
* @since 4.4.0
* @var string
*/
const EDITABLE = 'POST, PUT, PATCH';
/**
* Alias for DELETE transport method.
*
* @since 4.4.0
* @var string
*/
const DELETABLE = 'DELETE';
/**
* Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
*
* @since 4.4.0
* @var string
*/
const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
/**
* Namespaces registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $namespaces = array();
/**
* Endpoints registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $endpoints = array();
/**
* Options defined for the routes.
*
* @since 4.4.0
* @var array
*/
protected $route_options = array();
/**
* Caches embedded requests.
*
* @since 5.4.0
* @var array
*/
protected $embed_cache = array();
/**
* Stores request objects that are currently being handled.
*
* @since 6.5.0
* @var array
*/
protected $dispatching_requests = array();
/**
* Instantiates the REST server.
*
* @since 4.4.0
*/
public function __construct() {
$this->endpoints = array(
// Meta endpoints.
'/' => array(
'callback' => array( $this, 'get_index' ),
'methods' => 'GET',
'args' => array(
'context' => array(
'default' => 'view',
),
),
),
'/batch/v1' => array(
'callback' => array( $this, 'serve_batch_request_v1' ),
'methods' => 'POST',
'args' => array(
'validation' => array(
'type' => 'string',
'enum' => array( 'require-all-validate', 'normal' ),
'default' => 'normal',
),
'requests' => array(
'required' => true,
'type' => 'array',
'maxItems' => $this->get_max_batch_size(),
'items' => array(
'type' => 'object',
'properties' => array(
'method' => array(
'type' => 'string',
'enum' => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
'default' => 'POST',
),
'path' => array(
'type' => 'string',
'required' => true,
),
'body' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => true,
),
'headers' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => array(
'type' => array( 'string', 'array' ),
'items' => array(
'type' => 'string',
),
),
),
),
),
),
),
),
);
}
/**
* Checks the authentication headers if supplied.
*
* @since 4.4.0
*
* @return WP_Error|null|true WP_Error indicates unsuccessful login, null indicates successful
* or no authentication provided
*/
public function check_authentication() {
/**
* Filters REST API authentication errors.
*
* This is used to pass a WP_Error from an authentication method back to
* the API.
*
* Authentication methods should check first if they're being used, as
* multiple authentication methods can be enabled on a site (cookies,
* HTTP basic auth, OAuth). If the authentication method hooked in is
* not actually being attempted, null should be returned to indicate
* another authentication method should check instead. Similarly,
* callbacks should ensure the value is `null` before checking for
* errors.
*
* A WP_Error instance can be returned if an error occurs, and this should
* match the format used by API methods internally (that is, the `status`
* data should be used). A callback can return `true` to indicate that
* the authentication method was used, and it succeeded.
*
* @since 4.4.0
*
* @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
* method wasn't used, true if authentication succeeded.
*/
return apply_filters( 'rest_authentication_errors', null );
}
/**
* Converts an error to a response object.
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behavior, as it is represented as a
* list in JSON rather than an object/map.
*
* @since 4.4.0
* @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}.
*
* @param WP_Error $error WP_Error instance.
* @return WP_REST_Response List of associative arrays with code and message keys.
*/
protected function error_to_response( $error ) {
return rest_convert_error_to_response( $error );
}
/**
* Retrieves an appropriate error representation in JSON.
*
* Note: This should only be used in WP_REST_Server::serve_request(), as it
* cannot handle WP_Error internally. All callbacks and other internal methods
* should instead return a WP_Error with the data set to an array that includes
* a 'status' key, with the value being the HTTP status to send.
*
* @since 4.4.0
*
* @param string $code WP_Error-style code.
* @param string $message Human-readable message.
* @param int $status Optional. HTTP status code to send. Default null.
* @return string JSON representation of the error
*/
protected function json_error( $code, $message, $status = null ) {
if ( $status ) {
$this->set_status( $status );
}
$error = compact( 'code', 'message' );
return wp_json_encode( $error );
}
/**
* Gets the encoding options passed to {@see wp_json_encode}.
*
* @since 6.1.0
*
* @param \WP_REST_Request $request The current request object.
*
* @return int The JSON encode options.
*/
protected function get_json_encode_options( WP_REST_Request $request ) {
$options = 0;
if ( $request->has_param( '_pretty' ) ) {
$options |= JSON_PRETTY_PRINT;
}
/**
* Filters the JSON encoding options used to send the REST API response.
*
* @since 6.1.0
*
* @param int $options JSON encoding options {@see json_encode()}.
* @param WP_REST_Request $request Current request object.
*/
return apply_filters( 'rest_json_encode_options', $options, $request );
}
/**
* Handles serving a REST 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 4.4.0
*
* @see WP_REST_Server::dispatch()
*
* @global WP_User $current_user The currently authenticated user.
*
* @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @return null|false Null if not served and a HEAD request, false otherwise.
*/
public function serve_request( $path = null ) {
/* @var WP_User|null $current_user */
global $current_user;
if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
/*
* If there is no current user authenticated via other means, clear
* the cached lack of user, so that an authenticate check can set it
* properly.
*
* This is done because for authentications such as Application
* Passwords, we don't want it to be accepted unless the current HTTP
* request is a REST API request, which can't always be identified early
* enough in evaluation.
*/
$current_user = null;
}
/**
* Filters whether JSONP is enabled for the REST API.
*
* @since 4.4.0
*
* @param bool $jsonp_enabled Whether JSONP is enabled. Default true.
*/
$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
$jsonp_callback = false;
if ( isset( $_GET['_jsonp'] ) ) {
$jsonp_callback = $_GET['_jsonp'];
}
$content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json';
$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
$this->send_header( 'X-Robots-Tag', 'noindex' );
$api_root = get_rest_url();
if ( ! empty( $api_root ) ) {
$this->send_header( 'Link', '<' . sanitize_url( $api_root ) . '>; rel="https://api.w.org/"' );
}
/*
* Mitigate possible JSONP Flash attacks.
*
* https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
*/
$this->send_header( 'X-Content-Type-Options', 'nosniff' );
/**
* Filters whether the REST API is enabled.
*
* @since 4.4.0
* @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
* restrict access to the REST API.
*
* @param bool $rest_enabled Whether the REST API is enabled. Default true.
*/
apply_filters_deprecated(
'rest_enabled',
array( true ),
'4.7.0',
'rest_authentication_errors',
sprintf(
/* translators: %s: rest_authentication_errors */
__( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ),
'rest_authentication_errors'
)
);
if ( $jsonp_callback ) {
if ( ! $jsonp_enabled ) {
echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
return false;
}
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
return false;
}
}
if ( empty( $path ) ) {
if ( isset( $_SERVER['PATH_INFO'] ) ) {
$path = $_SERVER['PATH_INFO'];
} else {
$path = '/';
}
}
$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
$request->set_query_params( wp_unslash( $_GET ) );
$request->set_body_params( wp_unslash( $_POST ) );
$request->set_file_params( $_FILES );
$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
$request->set_body( self::get_raw_data() );
/*
* HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
* $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
* header.
*/
$method_overridden = false;
if ( isset( $_GET['_method'] ) ) {
$request->set_method( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
$method_overridden = true;
}
$expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' );
/**
* Filters the list of response headers that are exposed to REST API CORS requests.
*
* @since 5.5.0
* @since 6.3.0 The `$request` parameter was added.
*
* @param string[] $expose_headers The list of response headers to expose.
* @param WP_REST_Request $request The request in context.
*/
$expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers, $request );
$this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) );
$allow_headers = array(
'Authorization',
'X-WP-Nonce',
'Content-Disposition',
'Content-MD5',
'Content-Type',
);
/**
* Filters the list of request headers that are allowed for REST API CORS requests.
*
* The allowed headers are passed to the browser to specify which
* headers can be passed to the REST API. By default, we allow the
* Content-* headers needed to upload files to the media endpoints.
* As well as the Authorization and Nonce headers for allowing authentication.
*
* @since 5.5.0
* @since 6.3.0 The `$request` parameter was added.
*
* @param string[] $allow_headers The list of request headers to allow.
* @param WP_REST_Request $request The request in context.
*/
$allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers, $request );
$this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );
$result = $this->check_authentication();
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch( $request );
}
// Normalize to either WP_Error or WP_REST_Response...
$result = rest_ensure_response( $result );
// ...then convert WP_Error across.
if ( is_wp_error( $result ) ) {
$result = $this->error_to_response( $result );
}
/**
* Filters the REST API response.
*
* Allows modification of the response before returning.
*
* @since 4.4.0
* @since 4.5.0 Applied to embedded responses.
*
* @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
// Wrap the response in an envelope if asked for.
if ( isset( $_GET['_envelope'] ) ) {
$embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
$result = $this->envelope_response( $result, $embed );
}
// Send extra data from response objects.
$headers = $result->get_headers();
$this->send_headers( $headers );
$code = $result->get_status();
$this->set_status( $code );
/**
* Filters whether to send no-cache headers on a REST API request.
*
* @since 4.4.0
* @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from wp-includes/rest-api.php.
*
* @param bool $rest_send_nocache_headers Whether to send no-cache headers.
*/
$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
/*
* Send no-cache headers if $send_no_cache_headers is true,
* OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code.
*/
if ( $send_no_cache_headers || ( true === $method_overridden && str_starts_with( $code, '4' ) ) ) {
foreach ( wp_get_nocache_headers() as $header => $header_value ) {
if ( empty( $header_value ) ) {
$this->remove_header( $header );
} else {
$this->send_header( $header, $header_value );
}
}
}
/**
* Filters whether the REST API request has already been served.
*
* Allow sending the request manually - by returning true, the API result
* will not be sent to the client.
*
* @since 4.4.0
*
* @param bool $served Whether the request has already been served.
* Default false.
* @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`.
* @param WP_REST_Request $request Request used to generate the response.
* @param WP_REST_Server $server Server instance.
*/
$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
if ( ! $served ) {
if ( 'HEAD' === $request->get_method() ) {
return null;
}
// Embed links inside the request.
$embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
$result = $this->response_to_data( $result, $embed );
/**
* Filters the REST API response.
*
* Allows modification of the response data after inserting
* embedded data (if any) and before echoing the response data.
*
* @since 4.8.1
*
* @param array $result Response data to send to the client.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
// The 204 response shouldn't have a body.
if ( 204 === $code || null === $result ) {
return null;
}
$result = wp_json_encode( $result, $this->get_json_encode_options( $request ) );
$json_error_message = $this->get_json_last_error();
if ( $json_error_message ) {
$this->set_status( 500 );
$json_error_obj = new WP_Error(
'rest_encode_error',
$json_error_message,
array( 'status' => 500 )
);
$result = $this->error_to_response( $json_error_obj );
$result = wp_json_encode( $result->data, $this->get_json_encode_options( $request ) );
}
if ( $jsonp_callback ) {
// Prepend '/**/' to mitigate possible JSONP Flash attacks.
// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
echo '/**/' . $jsonp_callback . '(' . $result . ')';
} else {
echo $result;
}
}
return null;
}
/**
* Converts a response to data to send.
*
* @since 4.4.0
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param WP_REST_Response $response Response object.
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
* @return array {
* Data with sub-requests embedded.
*
* @type array $_links Links.
* @type array $_embedded Embedded objects.
* }
*/
public function response_to_data( $response, $embed ) {
$data = $response->get_data();
$links = self::get_compact_response_links( $response );
if ( ! empty( $links ) ) {
// Convert links to part of the data.
$data['_links'] = $links;
}
if ( $embed ) {
$this->embed_cache = array();
// Determine if this is a numeric array.
if ( wp_is_numeric_array( $data ) ) {
foreach ( $data as $key => $item ) {
$data[ $key ] = $this->embed_links( $item, $embed );
}
} else {
$data = $this->embed_links( $data, $embed );
}
$this->embed_cache = array();
}
return $data;
}
/**
* Retrieves links from a response.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
public static function get_response_links( $response ) {
$links = $response->get_links();
if ( empty( $links ) ) {
return array();
}
// Convert links to part of the data.
$data = array();
foreach ( $links as $rel => $items ) {
$data[ $rel ] = array();
foreach ( $items as $item ) {
$attributes = $item['attributes'];
$attributes['href'] = $item['href'];
$data[ $rel ][] = $attributes;
}
}
return $data;
}
/**
* Retrieves the CURIEs (compact URIs) used for relations.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.5.0
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
public static function get_compact_response_links( $response ) {
$links = self::get_response_links( $response );
if ( empty( $links ) ) {
return array();
}
$curies = $response->get_curies();
$used_curies = array();
foreach ( $links as $rel => $items ) {
// Convert $rel URIs to their compact versions if they exist.
foreach ( $curies as $curie ) {
$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
if ( ! str_starts_with( $rel, $href_prefix ) ) {
continue;
}
// Relation now changes from '$uri' to '$curie:$relation'.
$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
preg_match( '!' . $rel_regex . '!', $rel, $matches );
if ( $matches ) {
$new_rel = $curie['name'] . ':' . $matches[1];
$used_curies[ $curie['name'] ] = $curie;
$links[ $new_rel ] = $items;
unset( $links[ $rel ] );
break;
}
}
}
// Push the curies onto the start of the links array.
if ( $used_curies ) {
$links['curies'] = array_values( $used_curies );
}
return $links;
}
/**
* Embeds the links from the data into the request.
*
* @since 4.4.0
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param array $data Data from the request.
* @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
* @return array {
* Data with sub-requests embedded.
*
* @type array $_links Links.
* @type array $_embedded Embedded objects.
* }
*/
protected function embed_links( $data, $embed = true ) {
if ( empty( $data['_links'] ) ) {
return $data;
}
$embedded = array();
foreach ( $data['_links'] as $rel => $links ) {
/*
* If a list of relations was specified, and the link relation
* is not in the list of allowed relations, don't process the link.
*/
if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
continue;
}
$embeds = array();
foreach ( $links as $item ) {
// Determine if the link is embeddable.
if ( empty( $item['embeddable'] ) ) {
// Ensure we keep the same order.
$embeds[] = array();
continue;
}
if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) {
// Run through our internal routing and serve.
$request = WP_REST_Request::from_url( $item['href'] );
if ( ! $request ) {
$embeds[] = array();
continue;
}
// Embedded resources get passed context=embed.
if ( empty( $request['context'] ) ) {
$request['context'] = 'embed';
}
if ( empty( $request['per_page'] ) ) {
$matched = $this->match_request_to_handler( $request );
if ( ! is_wp_error( $matched ) && isset( $matched[1]['args']['per_page']['maximum'] ) ) {
$request['per_page'] = (int) $matched[1]['args']['per_page']['maximum'];
}
}
$response = $this->dispatch( $request );
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
$this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false );
}
$embeds[] = $this->embed_cache[ $item['href'] ];
}
// Determine if any real links were found.
$has_links = count( array_filter( $embeds ) );
if ( $has_links ) {
$embedded[ $rel ] = $embeds;
}
}
if ( ! empty( $embedded ) ) {
$data['_embedded'] = $embedded;
}
return $data;
}
/**
* Wraps the response in an envelope.
*
* The enveloping technique is used to work around browser/client
* compatibility issues. Essentially, it converts the full HTTP response to
* data instead.
*
* @since 4.4.0
* @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param WP_REST_Response $response Response object.
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
* @return WP_REST_Response New response with wrapped data
*/
public function envelope_response( $response, $embed ) {
$envelope = array(
'body' => $this->response_to_data( $response, $embed ),
'status' => $response->get_status(),
'headers' => $response->get_headers(),
);
/**
* Filters the enveloped form of a REST API response.
*
* @since 4.4.0
*
* @param array $envelope {
* Envelope data.
*
* @type array $body Response data.
* @type int $status The 3-digit HTTP status code.
* @type array $headers Map of header name to header value.
* }
* @param WP_REST_Response $response Original response data.
*/
$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
// Ensure it's still a response and return.
return rest_ensure_response( $envelope );
}
/**
* Registers a route to the server.
*
* @since 4.4.0
*
* @param string $route_namespace Namespace.
* @param string $route The REST route.
* @param array $route_args Route arguments.
* @param bool $override Optional. Whether the route should be overridden if it already exists.
* Default false.
*/
public function register_route( $route_namespace, $route, $route_args, $override = false ) {
if ( ! isset( $this->namespaces[ $route_namespace ] ) ) {
$this->namespaces[ $route_namespace ] = array();
$this->register_route(
$route_namespace,
'/' . $route_namespace,
array(
array(
'methods' => self::READABLE,
'callback' => array( $this, 'get_namespace_index' ),
'args' => array(
'namespace' => array(
'default' => $route_namespace,
),
'context' => array(
'default' => 'view',
),
),
),
)
);
}
// Associative to avoid double-registration.
$this->namespaces[ $route_namespace ][ $route ] = true;
$route_args['namespace'] = $route_namespace;
if ( $override || empty( $this->endpoints[ $route ] ) ) {
$this->endpoints[ $route ] = $route_args;
} else {
$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
}
}
/**
* Retrieves 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 4.4.0
* @since 5.4.0 Added `$route_namespace` parameter.
*
* @param string $route_namespace Optionally, only return routes in the given namespace.
* @return array `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
*/
public function get_routes( $route_namespace = '' ) {
$endpoints = $this->endpoints;
if ( $route_namespace ) {
$endpoints = wp_list_filter( $endpoints, array( 'namespace' => $route_namespace ) );
}
/**
* Filters the array of available REST API endpoints.
*
* @since 4.4.0
*
* @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
* to an array of callbacks for the endpoint. These take the format
* `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ).
*/
$endpoints = apply_filters( 'rest_endpoints', $endpoints );
// Normalize the endpoints.
$defaults = array(
'methods' => '',
'accept_json' => false,
'accept_raw' => false,
'show_in_index' => true,
'args' => array(),
);
foreach ( $endpoints as $route => &$handlers ) {
if ( isset( $handlers['callback'] ) ) {
// Single endpoint, add one deeper.
$handlers = array( $handlers );
}
if ( ! isset( $this->route_options[ $route ] ) ) {
$this->route_options[ $route ] = array();
}
foreach ( $handlers as $key => &$handler ) {
if ( ! is_numeric( $key ) ) {
// Route option, move it to the options.
$this->route_options[ $route ][ $key ] = $handler;
unset( $handlers[ $key ] );
continue;
}
$handler = wp_parse_args( $handler, $defaults );
// Allow comma-separated HTTP methods.
if ( is_string( $handler['methods'] ) ) {
$methods = explode( ',', $handler['methods'] );
} elseif ( is_array( $handler['methods'] ) ) {
$methods = $handler['methods'];
} else {
$methods = array();
}
$handler['methods'] = array();
foreach ( $methods as $method ) {
$method = strtoupper( trim( $method ) );
$handler['methods'][ $method ] = true;
}
}
}
return $endpoints;
}
/**
* Retrieves namespaces registered on the server.
*
* @since 4.4.0
*
* @return string[] List of registered namespaces.
*/
public function get_namespaces() {
return array_keys( $this->namespaces );
}
/**
* Retrieves specified options for a route.
*
* @since 4.4.0
*
* @param string $route Route pattern to fetch options for.
* @return array|null Data as an associative array if found, or null if not found.
*/
public function get_route_options( $route ) {
if ( ! isset( $this->route_options[ $route ] ) ) {
return null;
}
return $this->route_options[ $route ];
}
/**
* Matches the request to a callback and call it.
*
* @since 4.4.0
*
* @param WP_REST_Request $request Request to attempt dispatching.
* @return WP_REST_Response Response returned by the callback.
*/
public function dispatch( $request ) {
$this->dispatching_requests[] = $request;
/**
* Filters the pre-calculated result of a REST API dispatch request.
*
* Allow hijacking the request before dispatching by returning a non-empty. The returned value
* will be used to serve the request instead.
*
* @since 4.4.0
*
* @param mixed $result Response to replace the requested version with. Can be anything
* a normal endpoint can return, or null to not hijack the request.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
if ( ! empty( $result ) ) {
// Normalize to either WP_Error or WP_REST_Response...
$result = rest_ensure_response( $result );
// ...then convert WP_Error across.
if ( is_wp_error( $result ) ) {
$result = $this->error_to_response( $result );
}
array_pop( $this->dispatching_requests );
return $result;
}
$error = null;
$matched = $this->match_request_to_handler( $request );
if ( is_wp_error( $matched ) ) {
$response = $this->error_to_response( $matched );
array_pop( $this->dispatching_requests );
return $response;
}
list( $route, $handler ) = $matched;
if ( ! is_callable( $handler['callback'] ) ) {
$error = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid.' ),
array( 'status' => 500 )
);
}
if ( ! is_wp_error( $error ) ) {
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
}
$response = $this->respond_to_request( $request, $route, $handler, $error );
array_pop( $this->dispatching_requests );
return $response;
}
/**
* Returns whether the REST server is currently dispatching / responding to a request.
*
* This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
*
* @since 6.5.0
*
* @return bool Whether the REST server is currently handling a request.
*/
public function is_dispatching() {
return (bool) $this->dispatching_requests;
}
/**
* Matches a request object to its handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
*/
protected function match_request_to_handler( $request ) {
$method = $request->get_method();
$path = $request->get_route();
$with_namespace = array();
foreach ( $this->get_namespaces() as $namespace ) {
if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) {
$with_namespace[] = $this->get_routes( $namespace );
}
}
if ( $with_namespace ) {
$routes = array_merge( ...$with_namespace );
} else {
$routes = $this->get_routes();
}
foreach ( $routes as $route => $handlers ) {
$match = preg_match( '@^' . $route . '$@i', $path, $matches );
if ( ! $match ) {
continue;
}
$args = array();
foreach ( $matches as $param => $value ) {
if ( ! is_int( $param ) ) {
$args[ $param ] = $value;
}
}
foreach ( $handlers as $handler ) {
$callback = $handler['callback'];
// Fallback to GET method if no HEAD method is registered.
$checked_method = $method;
if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
$checked_method = 'GET';
}
if ( empty( $handler['methods'][ $checked_method ] ) ) {
continue;
}
if ( ! is_callable( $callback ) ) {
return array( $route, $handler );
}
$request->set_url_params( $args );
$request->set_attributes( $handler );
$defaults = array();
foreach ( $handler['args'] as $arg => $options ) {
if ( isset( $options['default'] ) ) {
$defaults[ $arg ] = $options['default'];
}
}
$request->set_default_params( $defaults );
return array( $route, $handler );
}
}
return new WP_Error(
'rest_no_route',
__( 'No route was found matching the URL and request method.' ),
array( 'status' => 404 )
);
}
/**
* Dispatches the request to the callback handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @param string $route The matched route regex.
* @param array $handler The matched route handler.
* @param WP_Error|null $response The current error object if any.
* @return WP_REST_Response
*/
protected function respond_to_request( $request, $route, $handler, $response ) {
/**
* Filters the response before executing any REST API callbacks.
*
* Allows plugins to perform additional validation after a
* request is initialized and matched to a registered route,
* but before it is executed.
*
* Note that this filter will not be called for requests that
* fail to authenticate or match to a registered route.
*
* @since 4.7.0
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
// Check permission specified on the route.
if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
$permission = call_user_func( $handler['permission_callback'], $request );
if ( is_wp_error( $permission ) ) {
$response = $permission;
} elseif ( false === $permission || null === $permission ) {
$response = new WP_Error(
'rest_forbidden',
__( 'Sorry, you are not allowed to do that.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
if ( ! is_wp_error( $response ) ) {
/**
* Filters the REST API dispatch request result.
*
* Allow plugins to override dispatching the request.
*
* @since 4.4.0
* @since 4.5.0 Added `$route` and `$handler` parameters.
*
* @param mixed $dispatch_result Dispatch result, will be used if not empty.
* @param WP_REST_Request $request Request used to generate the response.
* @param string $route Route matched for the request.
* @param array $handler Route handler used for the request.
*/
$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
// Allow plugins to halt the request via this filter.
if ( null !== $dispatch_result ) {
$response = $dispatch_result;
} else {
$response = call_user_func( $handler['callback'], $request );
}
}
/**
* Filters the response immediately after executing any REST API
* callbacks.
*
* Allows plugins to perform any needed cleanup, for example,
* to undo changes made during the {@see 'rest_request_before_callbacks'}
* filter.
*
* Note that this filter will not be called for requests that
* fail to authenticate or match to a registered route.
*
* Note that an endpoint's `permission_callback` can still be
* called after this filter - see `rest_send_allow_header()`.
*
* @since 4.7.0
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
} else {
$response = rest_ensure_response( $response );
}
$response->set_matched_route( $route );
$response->set_matched_handler( $handler );
return $response;
}
/**
* Returns if an error occurred during most recent JSON encode/decode.
*
* Strings to be translated will be in format like
* "Encoding error: Maximum stack depth exceeded".
*
* @since 4.4.0
*
* @return false|string Boolean false or string error message.
*/
protected function get_json_last_error() {
$last_error_code = json_last_error();
if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
return false;
}
return json_last_error_msg();
}
/**
* Retrieves the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 4.4.0
*
* @param array $request {
* Request.
*
* @type string $context Context.
* }
* @return WP_REST_Response The API root index data.
*/
public function get_index( $request ) {
// General site data.
$available = array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'url' => get_option( 'siteurl' ),
'home' => home_url(),
'gmt_offset' => get_option( 'gmt_offset' ),
'timezone_string' => get_option( 'timezone_string' ),
'namespaces' => array_keys( $this->namespaces ),
'authentication' => array(),
'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
);
$response = new WP_REST_Response( $available );
$fields = isset( $request['_fields'] ) ? $request['_fields'] : '';
$fields = wp_parse_list( $fields );
if ( empty( $fields ) ) {
$fields[] = '_links';
}
if ( $request->has_param( '_embed' ) ) {
$fields[] = '_embedded';
}
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' );
$this->add_active_theme_link_to_index( $response );
$this->add_site_logo_to_index( $response );
$this->add_site_icon_to_index( $response );
} else {
if ( rest_is_field_included( 'site_logo', $fields ) ) {
$this->add_site_logo_to_index( $response );
}
if ( rest_is_field_included( 'site_icon', $fields ) || rest_is_field_included( 'site_icon_url', $fields ) ) {
$this->add_site_icon_to_index( $response );
}
}
/**
* Filters the REST API root index data.
*
* This contains the data describing the API. This includes information
* about supported authentication schemes, supported namespaces, routes
* available on the API, and a small amount of data about the site.
*
* @since 4.4.0
* @since 6.0.0 Added `$request` parameter.
*
* @param WP_REST_Response $response Response data.
* @param WP_REST_Request $request Request data.
*/
return apply_filters( 'rest_index', $response, $request );
}
/**
* Adds a link to the active theme for users who have proper permissions.
*
* @since 5.7.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
$should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );
if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
$should_add = true;
}
if ( ! $should_add ) {
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
$should_add = true;
break;
}
}
}
if ( $should_add ) {
$theme = wp_get_theme();
$response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
}
}
/**
* Exposes the site logo through the WordPress REST API.
*
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.8.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_site_logo_to_index( WP_REST_Response $response ) {
$site_logo_id = get_theme_mod( 'custom_logo', 0 );
$this->add_image_to_index( $response, $site_logo_id, 'site_logo' );
}
/**
* Exposes the site icon through the WordPress REST API.
*
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.9.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_site_icon_to_index( WP_REST_Response $response ) {
$site_icon_id = get_option( 'site_icon', 0 );
$this->add_image_to_index( $response, $site_icon_id, 'site_icon' );
$response->data['site_icon_url'] = get_site_icon_url();
}
/**
* Exposes an image through the WordPress REST API.
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.9.0
*
* @param WP_REST_Response $response REST API response.
* @param int $image_id Image attachment ID.
* @param string $type Type of Image.
*/
protected function add_image_to_index( WP_REST_Response $response, $image_id, $type ) {
$response->data[ $type ] = (int) $image_id;
if ( $image_id ) {
$response->add_link(
'https://api.w.org/featuredmedia',
rest_url( rest_get_route_for_post( $image_id ) ),
array(
'embeddable' => true,
'type' => $type,
)
);
}
}
/**
* Retrieves the index for a namespace.
*
* @since 4.4.0
*
* @param WP_REST_Request $request REST request instance.
* @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
* WP_Error if the namespace isn't set.
*/
public function get_namespace_index( $request ) {
$namespace = $request['namespace'];
if ( ! isset( $this->namespaces[ $namespace ] ) ) {
return new WP_Error(
'rest_invalid_namespace',
__( 'The specified namespace could not be found.' ),
array( 'status' => 404 )
);
}
$routes = $this->namespaces[ $namespace ];
$endpoints = array_intersect_key( $this->get_routes(), $routes );
$data = array(
'namespace' => $namespace,
'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
);
$response = rest_ensure_response( $data );
// Link to the root index.
$response->add_link( 'up', rest_url( '/' ) );
/**
* Filters the REST API namespace index data.
*
* This typically is just the route data for the namespace, but you can
* add any data you'd like here.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response data.
* @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
*/
return apply_filters( 'rest_namespace_index', $response, $request );
}
/**
* Retrieves the publicly-visible data for routes.
*
* @since 4.4.0
*
* @param array $routes Routes to get data for.
* @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
* @return array[] Route data to expose in indexes, keyed by route.
*/
public function get_data_for_routes( $routes, $context = 'view' ) {
$available = array();
// Find the available routes.
foreach ( $routes as $route => $callbacks ) {
$data = $this->get_data_for_route( $route, $callbacks, $context );
if ( empty( $data ) ) {
continue;
}
/**
* Filters the publicly-visible data for a single REST API route.
*
* @since 4.4.0
*
* @param array $data Publicly-visible data for the route.
*/
$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
}
/**
* Filters the publicly-visible data for REST API routes.
*
* This data is exposed on indexes and can be used by clients or
* developers to investigate the site and find out how to use it. It
* acts as a form of self-documentation.
*
* @since 4.4.0
*
* @param array[] $available Route data to expose in indexes, keyed by route.
* @param array $routes Internal route data as an associative array.
*/
return apply_filters( 'rest_route_data', $available, $routes );
}
/**
* Retrieves publicly-visible data for the route.
*
* @since 4.4.0
*
* @param string $route Route to get data for.
* @param array $callbacks Callbacks to convert to data.
* @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
* @return array|null Data for the route, or null if no publicly-visible data.
*/
public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
$data = array(
'namespace' => '',
'methods' => array(),
'endpoints' => array(),
);
$allow_batch = false;
if ( isset( $this->route_options[ $route ] ) ) {
$options = $this->route_options[ $route ];
if ( isset( $options['namespace'] ) ) {
$data['namespace'] = $options['namespace'];
}
$allow_batch = isset( $options['allow_batch'] ) ? $options['allow_batch'] : false;
if ( isset( $options['schema'] ) && 'help' === $context ) {
$data['schema'] = call_user_func( $options['schema'] );
}
}
$allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() );
$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden.
if ( empty( $callback['show_in_index'] ) ) {
continue;
}
$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
$endpoint_data = array(
'methods' => array_keys( $callback['methods'] ),
);
$callback_batch = isset( $callback['allow_batch'] ) ? $callback['allow_batch'] : $allow_batch;
if ( $callback_batch ) {
$endpoint_data['allow_batch'] = $callback_batch;
}
if ( isset( $callback['args'] ) ) {
$endpoint_data['args'] = array();
foreach ( $callback['args'] as $key => $opts ) {
if ( is_string( $opts ) ) {
$opts = array( $opts => 0 );
} elseif ( ! is_array( $opts ) ) {
$opts = array();
}
$arg_data = array_intersect_key( $opts, $allowed_schema_keywords );
$arg_data['required'] = ! empty( $opts['required'] );
$endpoint_data['args'][ $key ] = $arg_data;
}
}
$data['endpoints'][] = $endpoint_data;
// For non-variable routes, generate links.
if ( ! str_contains( $route, '{' ) ) {
$data['_links'] = array(
'self' => array(
array(
'href' => rest_url( $route ),
),
),
);
}
}
if ( empty( $data['methods'] ) ) {
// No methods supported, hide the route.
return null;
}
return $data;
}
/**
* Gets the maximum number of requests that can be included in a batch.
*
* @since 5.6.0
*
* @return int The maximum requests.
*/
protected function get_max_batch_size() {
/**
* Filters the maximum number of REST API requests that can be included in a batch.
*
* @since 5.6.0
*
* @param int $max_size The maximum size.
*/
return apply_filters( 'rest_get_max_batch_size', 25 );
}
/**
* Serves the batch/v1 request.
*
* @since 5.6.0
*
* @param WP_REST_Request $batch_request The batch request object.
* @return WP_REST_Response The generated response object.
*/
public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
$requests = array();
foreach ( $batch_request['requests'] as $args ) {
$parsed_url = wp_parse_url( $args['path'] );
if ( false === $parsed_url ) {
$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );
continue;
}
$single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] );
if ( ! empty( $parsed_url['query'] ) ) {
$query_args = null; // Satisfy linter.
wp_parse_str( $parsed_url['query'], $query_args );
$single_request->set_query_params( $query_args );
}
if ( ! empty( $args['body'] ) ) {
$single_request->set_body_params( $args['body'] );
}
if ( ! empty( $args['headers'] ) ) {
$single_request->set_headers( $args['headers'] );
}
$requests[] = $single_request;
}
$matches = array();
$validation = array();
$has_error = false;
foreach ( $requests as $single_request ) {
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
$error = null;
if ( is_wp_error( $match ) ) {
$error = $match;
}
if ( ! $error ) {
list( $route, $handler ) = $match;
if ( isset( $handler['allow_batch'] ) ) {
$allow_batch = $handler['allow_batch'];
} else {
$route_options = $this->get_route_options( $route );
$allow_batch = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false;
}
if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
$error = new WP_Error(
'rest_batch_not_allowed',
__( 'The requested route does not support batch requests.' ),
array( 'status' => 400 )
);
}
}
if ( ! $error ) {
$check_required = $single_request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
}
}
if ( ! $error ) {
$check_sanitized = $single_request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
if ( $error ) {
$has_error = true;
$validation[] = $error;
} else {
$validation[] = true;
}
}
$responses = array();
if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
foreach ( $validation as $valid ) {
if ( is_wp_error( $valid ) ) {
$responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
} else {
$responses[] = null;
}
}
return new WP_REST_Response(
array(
'failed' => 'validation',
'responses' => $responses,
),
WP_Http::MULTI_STATUS
);
}
foreach ( $requests as $i => $single_request ) {
$clean_request = clone $single_request;
$clean_request->set_url_params( array() );
$clean_request->set_attributes( array() );
$clean_request->set_default_params( array() );
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );
if ( empty( $result ) ) {
$match = $matches[ $i ];
$error = null;
if ( is_wp_error( $validation[ $i ] ) ) {
$error = $validation[ $i ];
}
if ( is_wp_error( $match ) ) {
$result = $this->error_to_response( $match );
} else {
list( $route, $handler ) = $match;
if ( ! $error && ! is_callable( $handler['callback'] ) ) {
$error = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid' ),
array( 'status' => 500 )
);
}
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
}
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );
$responses[] = $this->envelope_response( $result, false )->get_data();
}
return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
}
/**
* Sends an HTTP status code.
*
* @since 4.4.0
*
* @param int $code HTTP status.
*/
protected function set_status( $code ) {
status_header( $code );
}
/**
* Sends an HTTP header.
*
* @since 4.4.0
*
* @param string $key Header key.
* @param string $value Header value.
*/
public function send_header( $key, $value ) {
/*
* Sanitize as per RFC2616 (Section 4.2):
*
* Any LWS that occurs between field-content MAY be replaced with a
* single SP before interpreting the field value or forwarding the
* message downstream.
*/
$value = preg_replace( '/\s+/', ' ', $value );
header( sprintf( '%s: %s', $key, $value ) );
}
/**
* Sends multiple HTTP headers.
*
* @since 4.4.0
*
* @param array $headers Map of header name to header value.
*/
public function send_headers( $headers ) {
foreach ( $headers as $key => $value ) {
$this->send_header( $key, $value );
}
}
/**
* Removes an HTTP header from the current response.
*
* @since 4.8.0
*
* @param string $key Header key.
*/
public function remove_header( $key ) {
header_remove( $key );
}
/**
* Retrieves the raw request entity (body).
*
* @since 4.4.0
*
* @global string $HTTP_RAW_POST_DATA Raw post data.
*
* @return string Raw request data.
*/
public static function get_raw_data() {
// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
global $HTTP_RAW_POST_DATA;
// $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0.
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}
return $HTTP_RAW_POST_DATA;
// phpcs:enable
}
/**
* Extracts headers from a PHP-style $_SERVER array.
*
* @since 4.4.0
*
* @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 ( str_starts_with( $key, 'HTTP_' ) ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) {
/*
* In some server configurations, the authorization header is passed in this alternate location.
* Since it would not be passed in in both places we do not check for both headers and resolve.
*/
$headers['AUTHORIZATION'] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
}
endpoints/class-wp-rest-application-passwords-controller.php 0000644 00000057132 15153067731 0020511 0 ustar 00 <?php
/**
* REST API: WP_REST_Application_Passwords_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.6.0
*/
/**
* Core class to access a user's application passwords via the REST API.
*
* @since 5.6.0
*
* @see WP_REST_Controller
*/
class WP_REST_Application_Passwords_Controller extends WP_REST_Controller {
/**
* Application Passwords controller constructor.
*
* @since 5.6.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'users/(?P<user_id>(?:[\d]+|me))/application-passwords';
}
/**
* Registers the REST API routes for the application passwords controller.
*
* @since 5.6.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema(),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_items' ),
'permission_callback' => array( $this, 'delete_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/introspect',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_current_item' ),
'permission_callback' => array( $this, 'get_current_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<uuid>[\w\-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'list_app_passwords', $user->ID ) ) {
return new WP_Error(
'rest_cannot_list_application_passwords',
__( 'Sorry, you are not allowed to list application passwords for this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves a collection of application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
$response = array();
foreach ( $passwords as $password ) {
$response[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $password, $request )
);
}
return new WP_REST_Response( $response );
}
/**
* Checks if a given request has access to get a specific application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'read_app_password', $user->ID, $request['uuid'] ) ) {
return new WP_Error(
'rest_cannot_read_application_password',
__( 'Sorry, you are not allowed to read this application password.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves one application password from the collection.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$password = $this->get_application_password( $request );
if ( is_wp_error( $password ) ) {
return $password;
}
return $this->prepare_item_for_response( $password, $request );
}
/**
* Checks if a given request has access to create application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'create_app_password', $user->ID ) ) {
return new WP_Error(
'rest_cannot_create_application_passwords',
__( 'Sorry, you are not allowed to create application passwords for this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates an application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$prepared = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$created = WP_Application_Passwords::create_new_application_password( $user->ID, wp_slash( (array) $prepared ) );
if ( is_wp_error( $created ) ) {
return $created;
}
$password = $created[0];
$item = WP_Application_Passwords::get_user_application_password( $user->ID, $created[1]['uuid'] );
$item['new_password'] = WP_Application_Passwords::chunk_password( $password );
$fields_update = $this->update_additional_fields_for_object( $item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
/**
* Fires after a single application password is completely created or updated via the REST API.
*
* @since 5.6.0
*
* @param array $item Inserted or updated password item.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating an application password, false when updating.
*/
do_action( 'rest_after_insert_application_password', $item, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $item, $request );
$response->set_status( 201 );
$response->header( 'Location', $response->get_links()['self'][0]['href'] );
return $response;
}
/**
* Checks if a given request has access to update application passwords.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'edit_app_password', $user->ID, $request['uuid'] ) ) {
return new WP_Error(
'rest_cannot_edit_application_password',
__( 'Sorry, you are not allowed to edit this application password.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates an application password.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$item = $this->get_application_password( $request );
if ( is_wp_error( $item ) ) {
return $item;
}
$prepared = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared ) ) {
return $prepared;
}
$saved = WP_Application_Passwords::update_application_password( $user->ID, $item['uuid'], wp_slash( (array) $prepared ) );
if ( is_wp_error( $saved ) ) {
return $saved;
}
$fields_update = $this->update_additional_fields_for_object( $item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$item = WP_Application_Passwords::get_user_application_password( $user->ID, $item['uuid'] );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php */
do_action( 'rest_after_insert_application_password', $item, $request, false );
$request->set_param( 'context', 'edit' );
return $this->prepare_item_for_response( $item, $request );
}
/**
* Checks if a given request has access to delete all application passwords for a user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_items_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'delete_app_passwords', $user->ID ) ) {
return new WP_Error(
'rest_cannot_delete_application_passwords',
__( 'Sorry, you are not allowed to delete application passwords for this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes all application passwords for a user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_items( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$deleted = WP_Application_Passwords::delete_all_application_passwords( $user->ID );
if ( is_wp_error( $deleted ) ) {
return $deleted;
}
return new WP_REST_Response(
array(
'deleted' => true,
'count' => $deleted,
)
);
}
/**
* Checks if a given request has access to delete a specific application password for a user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'delete_app_password', $user->ID, $request['uuid'] ) ) {
return new WP_Error(
'rest_cannot_delete_application_password',
__( 'Sorry, you are not allowed to delete this application password.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes an application password for a user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$password = $this->get_application_password( $request );
if ( is_wp_error( $password ) ) {
return $password;
}
$request->set_param( 'context', 'edit' );
$previous = $this->prepare_item_for_response( $password, $request );
$deleted = WP_Application_Passwords::delete_application_password( $user->ID, $password['uuid'] );
if ( is_wp_error( $deleted ) ) {
return $deleted;
}
return new WP_REST_Response(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
}
/**
* Checks if a given request has access to get the currently used application password for a user.
*
* @since 5.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_current_item_permissions_check( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( get_current_user_id() !== $user->ID ) {
return new WP_Error(
'rest_cannot_introspect_app_password_for_non_authenticated_user',
__( 'The authenticated application password can only be introspected for the current user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves the application password being currently used for authentication of a user.
*
* @since 5.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_current_item( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$uuid = rest_get_authenticated_app_password();
if ( ! $uuid ) {
return new WP_Error(
'rest_no_authenticated_app_password',
__( 'Cannot introspect application password.' ),
array( 'status' => 404 )
);
}
$password = WP_Application_Passwords::get_user_application_password( $user->ID, $uuid );
if ( ! $password ) {
return new WP_Error(
'rest_application_password_not_found',
__( 'Application password not found.' ),
array( 'status' => 500 )
);
}
return $this->prepare_item_for_response( $password, $request );
}
/**
* Performs a permissions check for the request.
*
* @since 5.6.0
* @deprecated 5.7.0 Use `edit_user` directly or one of the specific meta capabilities introduced in 5.7.0.
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
protected function do_permissions_check( $request ) {
_deprecated_function( __METHOD__, '5.7.0' );
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'edit_user', $user->ID ) ) {
return new WP_Error(
'rest_cannot_manage_application_passwords',
__( 'Sorry, you are not allowed to manage application passwords for this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Prepares an application password for a create or update operation.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Request object.
* @return object|WP_Error The prepared item, or WP_Error object on failure.
*/
protected function prepare_item_for_database( $request ) {
$prepared = (object) array(
'name' => $request['name'],
);
if ( $request['app_id'] && ! $request['uuid'] ) {
$prepared->app_id = $request['app_id'];
}
/**
* Filters an application password before it is inserted via the REST API.
*
* @since 5.6.0
*
* @param stdClass $prepared An object representing a single application password prepared for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_pre_insert_application_password', $prepared, $request );
}
/**
* Prepares the application password for the REST response.
*
* @since 5.6.0
*
* @param array $item WordPress representation of the item.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$fields = $this->get_fields_for_response( $request );
$prepared = array(
'uuid' => $item['uuid'],
'app_id' => empty( $item['app_id'] ) ? '' : $item['app_id'],
'name' => $item['name'],
'created' => gmdate( 'Y-m-d\TH:i:s', $item['created'] ),
'last_used' => $item['last_used'] ? gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ) : null,
'last_ip' => $item['last_ip'] ? $item['last_ip'] : null,
);
if ( isset( $item['new_password'] ) ) {
$prepared['password'] = $item['new_password'];
}
$prepared = $this->add_additional_fields_to_object( $prepared, $request );
$prepared = $this->filter_response_by_context( $prepared, $request['context'] );
$response = new WP_REST_Response( $prepared );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $user, $item ) );
}
/**
* Filters the REST API response for an application password.
*
* @since 5.6.0
*
* @param WP_REST_Response $response The response object.
* @param array $item The application password array.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_application_password', $response, $item, $request );
}
/**
* Prepares links for the request.
*
* @since 5.6.0
*
* @param WP_User $user The requested user.
* @param array $item The application password.
* @return array The list of links.
*/
protected function prepare_links( WP_User $user, $item ) {
return array(
'self' => array(
'href' => rest_url(
sprintf(
'%s/users/%d/application-passwords/%s',
$this->namespace,
$user->ID,
$item['uuid']
)
),
),
);
}
/**
* Gets the requested user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return WP_User|WP_Error The WordPress user associated with the request, or a WP_Error if none found.
*/
protected function get_user( $request ) {
if ( ! wp_is_application_passwords_available() ) {
return new WP_Error(
'application_passwords_disabled',
__( 'Application passwords are not available.' ),
array( 'status' => 501 )
);
}
$error = new WP_Error(
'rest_user_invalid_id',
__( 'Invalid user ID.' ),
array( 'status' => 404 )
);
$id = $request['user_id'];
if ( 'me' === $id ) {
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
__( 'You are not currently logged in.' ),
array( 'status' => 401 )
);
}
$user = wp_get_current_user();
} else {
$id = (int) $id;
if ( $id <= 0 ) {
return $error;
}
$user = get_userdata( $id );
}
if ( empty( $user ) || ! $user->exists() ) {
return $error;
}
if ( is_multisite() && ! user_can( $user->ID, 'manage_sites' ) && ! is_user_member_of_blog( $user->ID ) ) {
return $error;
}
if ( ! wp_is_application_passwords_available_for_user( $user ) ) {
return new WP_Error(
'application_passwords_disabled_for_user',
__( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ),
array( 'status' => 501 )
);
}
return $user;
}
/**
* Gets the requested application password for a user.
*
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return array|WP_Error The application password details if found, a WP_Error otherwise.
*/
protected function get_application_password( $request ) {
$user = $this->get_user( $request );
if ( is_wp_error( $user ) ) {
return $user;
}
$password = WP_Application_Passwords::get_user_application_password( $user->ID, $request['uuid'] );
if ( ! $password ) {
return new WP_Error(
'rest_application_password_not_found',
__( 'Application password not found.' ),
array( 'status' => 404 )
);
}
return $password;
}
/**
* Retrieves the query params for the collections.
*
* @since 5.6.0
*
* @return array Query parameters for the collection.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Retrieves the application password's schema, conforming to JSON Schema.
*
* @since 5.6.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'application-password',
'type' => 'object',
'properties' => array(
'uuid' => array(
'description' => __( 'The unique identifier for the application password.' ),
'type' => 'string',
'format' => 'uuid',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'app_id' => array(
'description' => __( 'A UUID provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.' ),
'type' => 'string',
'format' => 'uuid',
'context' => array( 'view', 'edit', 'embed' ),
),
'name' => array(
'description' => __( 'The name of the application password.' ),
'type' => 'string',
'required' => true,
'context' => array( 'view', 'edit', 'embed' ),
'minLength' => 1,
'pattern' => '.*\S.*',
),
'password' => array(
'description' => __( 'The generated password. Only available after adding an application.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'created' => array(
'description' => __( 'The GMT date the application password was created.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'last_used' => array(
'description' => __( 'The GMT date the application password was last used.' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'last_ip' => array(
'description' => __( 'The IP address the application password was last used by.' ),
'type' => array( 'string', 'null' ),
'format' => 'ip',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-attachments-controller.php 0000644 00000132377 15153067731 0016503 0 ustar 00 <?php
/**
* REST API: WP_REST_Attachments_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core controller used to access attachments via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Posts_Controller
*/
class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
/**
* Whether the controller supports batching.
*
* @since 5.9.0
* @var false
*/
protected $allow_batch = false;
/**
* Registers the routes for attachments.
*
* @since 5.3.0
*
* @see register_rest_route()
*/
public function register_routes() {
parent::register_routes();
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'post_process_item' ),
'permission_callback' => array( $this, 'post_process_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the attachment.' ),
'type' => 'integer',
),
'action' => array(
'type' => 'string',
'enum' => array( 'create-image-subsizes' ),
'required' => true,
),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'edit_media_item' ),
'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
'args' => $this->get_edit_media_item_args(),
)
);
}
/**
* Determines the allowed query_vars for a get_items() response and
* prepares for WP_Query.
*
* @since 4.7.0
*
* @param array $prepared_args Optional. Array of prepared arguments. Default empty array.
* @param WP_REST_Request $request Optional. Request to prepare items for.
* @return array Array of query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = parent::prepare_items_query( $prepared_args, $request );
if ( empty( $query_args['post_status'] ) ) {
$query_args['post_status'] = 'inherit';
}
$media_types = $this->get_media_types();
if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
}
if ( ! empty( $request['mime_type'] ) ) {
$parts = explode( '/', $request['mime_type'] );
if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
$query_args['post_mime_type'] = $request['mime_type'];
}
}
// Filter query clauses to include filenames.
if ( isset( $query_args['s'] ) ) {
add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
}
return $query_args;
}
/**
* Checks if a given request has access to create an attachment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not.
*/
public function create_item_permissions_check( $request ) {
$ret = parent::create_item_permissions_check( $request );
if ( ! $ret || is_wp_error( $ret ) ) {
return $ret;
}
if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to upload media on this site.' ),
array( 'status' => 400 )
);
}
// Attaching media to a post requires ability to edit said post.
if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to upload media to this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates a single attachment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'Invalid parent type.' ),
array( 'status' => 400 )
);
}
$insert = $this->insert_attachment( $request );
if ( is_wp_error( $insert ) ) {
return $insert;
}
$schema = $this->get_item_schema();
// Extract by name.
$attachment_id = $insert['attachment_id'];
$file = $insert['file'];
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
}
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment_id );
if ( is_wp_error( $thumbnail_update ) ) {
return $thumbnail_update;
}
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $attachment_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$attachment = get_post( $attachment_id );
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$terms_update = $this->handle_terms( $attachment_id, $request );
if ( is_wp_error( $terms_update ) ) {
return $terms_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a single attachment is completely created or updated via the REST API.
*
* @since 5.0.0
*
* @param WP_Post $attachment Inserted or updated attachment object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating an attachment, false when updating.
*/
do_action( 'rest_after_insert_attachment', $attachment, $request, true );
wp_after_insert_post( $attachment, false, null );
if ( wp_is_serving_rest_request() ) {
/*
* Set a custom header with the attachment_id.
* Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
*/
header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
}
// Include media and image functions to get access to wp_generate_attachment_metadata().
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
/*
* Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta.
* At this point the server may run out of resources and post-processing of uploaded images may fail.
*/
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );
return $response;
}
/**
* Inserts the attachment post in the database. Does not update the attachment meta.
*
* @since 5.3.0
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
protected function insert_attachment( $request ) {
// Get the file via $_FILES or raw data.
$files = $request->get_file_params();
$headers = $request->get_headers();
if ( ! empty( $files ) ) {
$file = $this->upload_from_file( $files, $headers );
} else {
$file = $this->upload_from_data( $request->get_body(), $headers );
}
if ( is_wp_error( $file ) ) {
return $file;
}
$name = wp_basename( $file['file'] );
$name_parts = pathinfo( $name );
$name = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) );
$url = $file['url'];
$type = $file['type'];
$file = $file['file'];
// Include image functions to get access to wp_read_image_metadata().
require_once ABSPATH . 'wp-admin/includes/image.php';
// Use image exif/iptc data for title and caption defaults if possible.
$image_meta = wp_read_image_metadata( $file );
if ( ! empty( $image_meta ) ) {
if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
$request['title'] = $image_meta['title'];
}
if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
$request['caption'] = $image_meta['caption'];
}
}
$attachment = $this->prepare_item_for_database( $request );
$attachment->post_mime_type = $type;
$attachment->guid = $url;
if ( empty( $attachment->post_title ) ) {
$attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
}
// $post_parent is inherited from $attachment['post_parent'].
$id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );
if ( is_wp_error( $id ) ) {
if ( 'db_update_error' === $id->get_error_code() ) {
$id->add_data( array( 'status' => 500 ) );
} else {
$id->add_data( array( 'status' => 400 ) );
}
return $id;
}
$attachment = get_post( $id );
/**
* Fires after a single attachment is created or updated via the REST API.
*
* @since 4.7.0
*
* @param WP_Post $attachment Inserted or updated attachment
* object.
* @param WP_REST_Request $request The request sent to the API.
* @param bool $creating True when creating an attachment, false when updating.
*/
do_action( 'rest_insert_attachment', $attachment, $request, true );
return array(
'attachment_id' => $id,
'file' => $file,
);
}
/**
* Determines the featured media based on a request param.
*
* @since 6.5.0
*
* @param int $featured_media Featured Media ID.
* @param int $post_id Post ID.
* @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
*/
protected function handle_featured_media( $featured_media, $post_id ) {
$post_type = get_post_type( $post_id );
$thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' );
// Similar check as in wp_insert_post().
if ( ! $thumbnail_support && get_post_mime_type( $post_id ) ) {
if ( wp_attachment_is( 'audio', $post_id ) ) {
$thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' );
} elseif ( wp_attachment_is( 'video', $post_id ) ) {
$thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' );
}
}
if ( $thumbnail_support ) {
return parent::handle_featured_media( $featured_media, $post_id );
}
return new WP_Error(
'rest_no_featured_media',
sprintf(
/* translators: %s: attachment mime type */
__( 'This site does not support post thumbnails on attachments with MIME type %s.' ),
get_post_mime_type( $post_id )
),
array( 'status' => 400 )
);
}
/**
* Updates a single attachment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
*/
public function update_item( $request ) {
if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
return new WP_Error(
'rest_invalid_param',
__( 'Invalid parent type.' ),
array( 'status' => 400 )
);
}
$attachment_before = get_post( $request['id'] );
$response = parent::update_item( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$response = rest_ensure_response( $response );
$data = $response->get_data();
if ( isset( $request['alt_text'] ) ) {
update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
}
$attachment = get_post( $request['id'] );
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment->ID );
if ( is_wp_error( $thumbnail_update ) ) {
return $thumbnail_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $attachment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
do_action( 'rest_after_insert_attachment', $attachment, $request, false );
wp_after_insert_post( $attachment, true, $attachment_before );
$response = $this->prepare_item_for_response( $attachment, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Performs post processing on an attachment.
*
* @since 5.3.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
*/
public function post_process_item( $request ) {
switch ( $request['action'] ) {
case 'create-image-subsizes':
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_image_subsizes( $request['id'] );
break;
}
$request['context'] = 'edit';
return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
}
/**
* Checks if a given request can perform post processing on an attachment.
*
* @since 5.3.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function post_process_item_permissions_check( $request ) {
return $this->update_item_permissions_check( $request );
}
/**
* Checks if a given request has access to editing media.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function edit_media_item_permissions_check( $request ) {
if ( ! current_user_can( 'upload_files' ) ) {
return new WP_Error(
'rest_cannot_edit_image',
__( 'Sorry, you are not allowed to upload media on this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return $this->update_item_permissions_check( $request );
}
/**
* Applies edits to a media item and creates a new attachment record.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
*/
public function edit_media_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_id = $request['id'];
// This also confirms the attachment is an image.
$image_file = wp_get_original_image_path( $attachment_id );
$image_meta = wp_get_attachment_metadata( $attachment_id );
if (
! $image_meta ||
! $image_file ||
! wp_image_file_matches_image_meta( $request['src'], $image_meta, $attachment_id )
) {
return new WP_Error(
'rest_unknown_attachment',
__( 'Unable to get meta information for file.' ),
array( 'status' => 404 )
);
}
$supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' );
$mime_type = get_post_mime_type( $attachment_id );
if ( ! in_array( $mime_type, $supported_types, true ) ) {
return new WP_Error(
'rest_cannot_edit_file_type',
__( 'This type of file cannot be edited.' ),
array( 'status' => 400 )
);
}
// The `modifiers` param takes precedence over the older format.
if ( isset( $request['modifiers'] ) ) {
$modifiers = $request['modifiers'];
} else {
$modifiers = array();
if ( ! empty( $request['rotation'] ) ) {
$modifiers[] = array(
'type' => 'rotate',
'args' => array(
'angle' => $request['rotation'],
),
);
}
if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) {
$modifiers[] = array(
'type' => 'crop',
'args' => array(
'left' => $request['x'],
'top' => $request['y'],
'width' => $request['width'],
'height' => $request['height'],
),
);
}
if ( 0 === count( $modifiers ) ) {
return new WP_Error(
'rest_image_not_edited',
__( 'The image was not edited. Edit the image before applying the changes.' ),
array( 'status' => 400 )
);
}
}
/*
* If the file doesn't exist, attempt a URL fopen on the src link.
* This can occur with certain file replication plugins.
* Keep the original file path to get a modified name later.
*/
$image_file_to_edit = $image_file;
if ( ! file_exists( $image_file_to_edit ) ) {
$image_file_to_edit = _load_image_to_edit_path( $attachment_id );
}
$image_editor = wp_get_image_editor( $image_file_to_edit );
if ( is_wp_error( $image_editor ) ) {
return new WP_Error(
'rest_unknown_image_file_type',
__( 'Unable to edit this image.' ),
array( 'status' => 500 )
);
}
foreach ( $modifiers as $modifier ) {
$args = $modifier['args'];
switch ( $modifier['type'] ) {
case 'rotate':
// Rotation direction: clockwise vs. counter clockwise.
$rotate = 0 - $args['angle'];
if ( 0 !== $rotate ) {
$result = $image_editor->rotate( $rotate );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'rest_image_rotation_failed',
__( 'Unable to rotate this image.' ),
array( 'status' => 500 )
);
}
}
break;
case 'crop':
$size = $image_editor->get_size();
$crop_x = round( ( $size['width'] * $args['left'] ) / 100.0 );
$crop_y = round( ( $size['height'] * $args['top'] ) / 100.0 );
$width = round( ( $size['width'] * $args['width'] ) / 100.0 );
$height = round( ( $size['height'] * $args['height'] ) / 100.0 );
if ( $size['width'] !== $width && $size['height'] !== $height ) {
$result = $image_editor->crop( $crop_x, $crop_y, $width, $height );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'rest_image_crop_failed',
__( 'Unable to crop this image.' ),
array( 'status' => 500 )
);
}
}
break;
}
}
// Calculate the file name.
$image_ext = pathinfo( $image_file, PATHINFO_EXTENSION );
$image_name = wp_basename( $image_file, ".{$image_ext}" );
/*
* Do not append multiple `-edited` to the file name.
* The user may be editing a previously edited image.
*/
if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) {
// Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number.
$image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name );
} else {
// Append `-edited` before the extension.
$image_name .= '-edited';
}
$filename = "{$image_name}.{$image_ext}";
// Create the uploads sub-directory if needed.
$uploads = wp_upload_dir();
// Make the file name unique in the (new) upload directory.
$filename = wp_unique_filename( $uploads['path'], $filename );
// Save to disk.
$saved = $image_editor->save( $uploads['path'] . "/$filename" );
if ( is_wp_error( $saved ) ) {
return $saved;
}
// Create new attachment post.
$new_attachment_post = array(
'post_mime_type' => $saved['mime-type'],
'guid' => $uploads['url'] . "/$filename",
'post_title' => $image_name,
'post_content' => '',
);
// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
$attachment_post = get_post( $attachment_id );
if ( $attachment_post ) {
$new_attachment_post['post_content'] = $attachment_post->post_content;
$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
$new_attachment_post['post_title'] = $attachment_post->post_title;
}
$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );
if ( is_wp_error( $new_attachment_id ) ) {
if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
$new_attachment_id->add_data( array( 'status' => 500 ) );
} else {
$new_attachment_id->add_data( array( 'status' => 400 ) );
}
return $new_attachment_id;
}
// Copy the image alt text from the edited image.
$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
if ( ! empty( $image_alt ) ) {
// update_post_meta() expects slashed.
update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
}
if ( wp_is_serving_rest_request() ) {
/*
* Set a custom header with the attachment_id.
* Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
*/
header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id );
}
// Generate image sub-sizes and meta.
$new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] );
// Copy the EXIF metadata from the original attachment if not generated for the edited image.
if ( isset( $image_meta['image_meta'] ) && isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) {
// Merge but skip empty values.
foreach ( (array) $image_meta['image_meta'] as $key => $value ) {
if ( empty( $new_image_meta['image_meta'][ $key ] ) && ! empty( $value ) ) {
$new_image_meta['image_meta'][ $key ] = $value;
}
}
}
// Reset orientation. At this point the image is edited and orientation is correct.
if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) {
$new_image_meta['image_meta']['orientation'] = 1;
}
// The attachment_id may change if the site is exported and imported.
$new_image_meta['parent_image'] = array(
'attachment_id' => $attachment_id,
// Path to the originally uploaded image file relative to the uploads directory.
'file' => _wp_relative_upload_path( $image_file ),
);
/**
* Filters the meta data for the new image created by editing an existing image.
*
* @since 5.5.0
*
* @param array $new_image_meta Meta data for the new image.
* @param int $new_attachment_id Attachment post ID for the new image.
* @param int $attachment_id Attachment post ID for the edited (parent) image.
*/
$new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id );
wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );
$response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) );
return $response;
}
/**
* Prepares a single attachment for create or update.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_attachment = parent::prepare_item_for_database( $request );
// Attachment caption (post_excerpt internally).
if ( isset( $request['caption'] ) ) {
if ( is_string( $request['caption'] ) ) {
$prepared_attachment->post_excerpt = $request['caption'];
} elseif ( isset( $request['caption']['raw'] ) ) {
$prepared_attachment->post_excerpt = $request['caption']['raw'];
}
}
// Attachment description (post_content internally).
if ( isset( $request['description'] ) ) {
if ( is_string( $request['description'] ) ) {
$prepared_attachment->post_content = $request['description'];
} elseif ( isset( $request['description']['raw'] ) ) {
$prepared_attachment->post_content = $request['description']['raw'];
}
}
if ( isset( $request['post'] ) ) {
$prepared_attachment->post_parent = (int) $request['post'];
}
return $prepared_attachment;
}
/**
* Prepares a single attachment output for response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item Attachment object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$response = parent::prepare_item_for_response( $post, $request );
$fields = $this->get_fields_for_response( $request );
$data = $response->get_data();
if ( in_array( 'description', $fields, true ) ) {
$data['description'] = array(
'raw' => $post->post_content,
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'the_content', $post->post_content ),
);
}
if ( in_array( 'caption', $fields, true ) ) {
/** This filter is documented in wp-includes/post-template.php */
$caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
/** This filter is documented in wp-includes/post-template.php */
$caption = apply_filters( 'the_excerpt', $caption );
$data['caption'] = array(
'raw' => $post->post_excerpt,
'rendered' => $caption,
);
}
if ( in_array( 'alt_text', $fields, true ) ) {
$data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
}
if ( in_array( 'media_type', $fields, true ) ) {
$data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
}
if ( in_array( 'mime_type', $fields, true ) ) {
$data['mime_type'] = $post->post_mime_type;
}
if ( in_array( 'media_details', $fields, true ) ) {
$data['media_details'] = wp_get_attachment_metadata( $post->ID );
// Ensure empty details is an empty object.
if ( empty( $data['media_details'] ) ) {
$data['media_details'] = new stdClass();
} elseif ( ! empty( $data['media_details']['sizes'] ) ) {
foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {
if ( isset( $size_data['mime-type'] ) ) {
$size_data['mime_type'] = $size_data['mime-type'];
unset( $size_data['mime-type'] );
}
// Use the same method image_downsize() does.
$image_src = wp_get_attachment_image_src( $post->ID, $size );
if ( ! $image_src ) {
continue;
}
$size_data['source_url'] = $image_src[0];
}
$full_src = wp_get_attachment_image_src( $post->ID, 'full' );
if ( ! empty( $full_src ) ) {
$data['media_details']['sizes']['full'] = array(
'file' => wp_basename( $full_src[0] ),
'width' => $full_src[1],
'height' => $full_src[2],
'mime_type' => $post->post_mime_type,
'source_url' => $full_src[0],
);
}
} else {
$data['media_details']['sizes'] = new stdClass();
}
}
if ( in_array( 'post', $fields, true ) ) {
$data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
}
if ( in_array( 'source_url', $fields, true ) ) {
$data['source_url'] = wp_get_attachment_url( $post->ID );
}
if ( in_array( 'missing_image_sizes', $fields, true ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
$data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->filter_response_by_context( $data, $context );
$links = $response->get_links();
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
foreach ( $links as $rel => $rel_links ) {
foreach ( $rel_links as $link ) {
$response->add_link( $rel, $link['href'], $link['attributes'] );
}
}
/**
* Filters an attachment returned from the REST API.
*
* Allows modification of the attachment right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The original attachment post.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
}
/**
* Retrieves the attachment's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema as an array.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = parent::get_item_schema();
$schema['properties']['alt_text'] = array(
'description' => __( 'Alternative text to display when attachment is not displayed.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
);
$schema['properties']['caption'] = array(
'description' => __( 'The attachment caption.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Caption for the attachment, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML caption for the attachment, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
$schema['properties']['description'] = array(
'description' => __( 'The attachment description.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Description for the attachment, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML description for the attachment, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
$schema['properties']['media_type'] = array(
'description' => __( 'Attachment type.' ),
'type' => 'string',
'enum' => array( 'image', 'file' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['mime_type'] = array(
'description' => __( 'The attachment MIME type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['media_details'] = array(
'description' => __( 'Details about the media file, specific to its type.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['post'] = array(
'description' => __( 'The ID for the associated post of the attachment.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
$schema['properties']['source_url'] = array(
'description' => __( 'URL to the original attachment file.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['missing_image_sizes'] = array(
'description' => __( 'List of the missing image sizes of the attachment.' ),
'type' => 'array',
'items' => array( 'type' => 'string' ),
'context' => array( 'edit' ),
'readonly' => true,
);
unset( $schema['properties']['password'] );
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Handles an upload via raw POST data.
*
* @since 4.7.0
*
* @param string $data Supplied file data.
* @param array $headers HTTP headers from the request.
* @return array|WP_Error Data from wp_handle_sideload().
*/
protected function upload_from_data( $data, $headers ) {
if ( empty( $data ) ) {
return new WP_Error(
'rest_upload_no_data',
__( 'No data supplied.' ),
array( 'status' => 400 )
);
}
if ( empty( $headers['content_type'] ) ) {
return new WP_Error(
'rest_upload_no_content_type',
__( 'No Content-Type supplied.' ),
array( 'status' => 400 )
);
}
if ( empty( $headers['content_disposition'] ) ) {
return new WP_Error(
'rest_upload_no_content_disposition',
__( 'No Content-Disposition supplied.' ),
array( 'status' => 400 )
);
}
$filename = self::get_filename_from_disposition( $headers['content_disposition'] );
if ( empty( $filename ) ) {
return new WP_Error(
'rest_upload_invalid_disposition',
__( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5( $data );
if ( $expected !== $actual ) {
return new WP_Error(
'rest_upload_hash_mismatch',
__( 'Content hash did not match expected.' ),
array( 'status' => 412 )
);
}
}
// Get the content-type.
$type = array_shift( $headers['content_type'] );
// Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload().
require_once ABSPATH . 'wp-admin/includes/file.php';
// Save the file.
$tmpfname = wp_tempnam( $filename );
$fp = fopen( $tmpfname, 'w+' );
if ( ! $fp ) {
return new WP_Error(
'rest_upload_file_error',
__( 'Could not open file handle.' ),
array( 'status' => 500 )
);
}
fwrite( $fp, $data );
fclose( $fp );
// Now, sideload it in.
$file_data = array(
'error' => null,
'tmp_name' => $tmpfname,
'name' => $filename,
'type' => $type,
);
$size_check = self::check_upload_size( $file_data );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}
$overrides = array(
'test_form' => false,
);
$sideloaded = wp_handle_sideload( $file_data, $overrides );
if ( isset( $sideloaded['error'] ) ) {
@unlink( $tmpfname );
return new WP_Error(
'rest_upload_sideload_error',
$sideloaded['error'],
array( 'status' => 500 )
);
}
return $sideloaded;
}
/**
* Parses filename from a Content-Disposition header value.
*
* As per RFC6266:
*
* content-disposition = "Content-Disposition" ":"
* disposition-type *( ";" disposition-parm )
*
* disposition-type = "inline" | "attachment" | disp-ext-type
* ; case-insensitive
* disp-ext-type = token
*
* disposition-parm = filename-parm | disp-ext-parm
*
* filename-parm = "filename" "=" value
* | "filename*" "=" ext-value
*
* disp-ext-parm = token "=" value
* | ext-token "=" ext-value
* ext-token = <the characters in token, followed by "*">
*
* @since 4.7.0
*
* @link https://tools.ietf.org/html/rfc2388
* @link https://tools.ietf.org/html/rfc6266
*
* @param string[] $disposition_header List of Content-Disposition header values.
* @return string|null Filename if available, or null if not found.
*/
public static function get_filename_from_disposition( $disposition_header ) {
// Get the filename.
$filename = null;
foreach ( $disposition_header as $value ) {
$value = trim( $value );
if ( ! str_contains( $value, ';' ) ) {
continue;
}
list( $type, $attr_parts ) = explode( ';', $value, 2 );
$attr_parts = explode( ';', $attr_parts );
$attributes = array();
foreach ( $attr_parts as $part ) {
if ( ! str_contains( $part, '=' ) ) {
continue;
}
list( $key, $value ) = explode( '=', $part, 2 );
$attributes[ trim( $key ) ] = trim( $value );
}
if ( empty( $attributes['filename'] ) ) {
continue;
}
$filename = trim( $attributes['filename'] );
// Unquote quoted filename, but after trimming.
if ( str_starts_with( $filename, '"' ) && str_ends_with( $filename, '"' ) ) {
$filename = substr( $filename, 1, -1 );
}
}
return $filename;
}
/**
* Retrieves the query params for collections of attachments.
*
* @since 4.7.0
*
* @return array Query parameters for the attachment collection as an array.
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['status']['default'] = 'inherit';
$params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' );
$media_types = $this->get_media_types();
$params['media_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular media type.' ),
'type' => 'string',
'enum' => array_keys( $media_types ),
);
$params['mime_type'] = array(
'default' => null,
'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
'type' => 'string',
);
return $params;
}
/**
* Handles an upload via multipart/form-data ($_FILES).
*
* @since 4.7.0
*
* @param array $files Data from the `$_FILES` superglobal.
* @param array $headers HTTP headers from the request.
* @return array|WP_Error Data from wp_handle_upload().
*/
protected function upload_from_file( $files, $headers ) {
if ( empty( $files ) ) {
return new WP_Error(
'rest_upload_no_data',
__( 'No data supplied.' ),
array( 'status' => 400 )
);
}
// Verify hash, if given.
if ( ! empty( $headers['content_md5'] ) ) {
$content_md5 = array_shift( $headers['content_md5'] );
$expected = trim( $content_md5 );
$actual = md5_file( $files['file']['tmp_name'] );
if ( $expected !== $actual ) {
return new WP_Error(
'rest_upload_hash_mismatch',
__( 'Content hash did not match expected.' ),
array( 'status' => 412 )
);
}
}
// Pass off to WP to handle the actual upload.
$overrides = array(
'test_form' => false,
);
// Bypasses is_uploaded_file() when running unit tests.
if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
$overrides['action'] = 'wp_handle_mock_upload';
}
$size_check = self::check_upload_size( $files['file'] );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}
// Include filesystem functions to get access to wp_handle_upload().
require_once ABSPATH . 'wp-admin/includes/file.php';
$file = wp_handle_upload( $files['file'], $overrides );
if ( isset( $file['error'] ) ) {
return new WP_Error(
'rest_upload_unknown_error',
$file['error'],
array( 'status' => 500 )
);
}
return $file;
}
/**
* Retrieves the supported media types.
*
* Media types are considered the MIME type category.
*
* @since 4.7.0
*
* @return array Array of supported media types.
*/
protected function get_media_types() {
$media_types = array();
foreach ( get_allowed_mime_types() as $mime_type ) {
$parts = explode( '/', $mime_type );
if ( ! isset( $media_types[ $parts[0] ] ) ) {
$media_types[ $parts[0] ] = array();
}
$media_types[ $parts[0] ][] = $mime_type;
}
return $media_types;
}
/**
* Determine if uploaded file exceeds space quota on multisite.
*
* Replicates check_upload_size().
*
* @since 4.9.8
*
* @param array $file $_FILES array for a given file.
* @return true|WP_Error True if can upload, error for errors.
*/
protected function check_upload_size( $file ) {
if ( ! is_multisite() ) {
return true;
}
if ( get_site_option( 'upload_space_check_disabled' ) ) {
return true;
}
$space_left = get_upload_space_available();
$file_size = filesize( $file['tmp_name'] );
if ( $space_left < $file_size ) {
return new WP_Error(
'rest_upload_limited_space',
/* translators: %s: Required disk space in kilobytes. */
sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ),
array( 'status' => 400 )
);
}
if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
return new WP_Error(
'rest_upload_file_too_big',
/* translators: %s: Maximum allowed file size in kilobytes. */
sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ),
array( 'status' => 400 )
);
}
// Include multisite admin functions to get access to upload_is_user_over_quota().
require_once ABSPATH . 'wp-admin/includes/ms.php';
if ( upload_is_user_over_quota( false ) ) {
return new WP_Error(
'rest_upload_user_quota_exceeded',
__( 'You have used your space quota. Please delete files before uploading.' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Gets the request args for the edit item route.
*
* @since 5.5.0
*
* @return array
*/
protected function get_edit_media_item_args() {
return array(
'src' => array(
'description' => __( 'URL to the edited image file.' ),
'type' => 'string',
'format' => 'uri',
'required' => true,
),
'modifiers' => array(
'description' => __( 'Array of image edits.' ),
'type' => 'array',
'minItems' => 1,
'items' => array(
'description' => __( 'Image edit.' ),
'type' => 'object',
'required' => array(
'type',
'args',
),
'oneOf' => array(
array(
'title' => __( 'Rotation' ),
'properties' => array(
'type' => array(
'description' => __( 'Rotation type.' ),
'type' => 'string',
'enum' => array( 'rotate' ),
),
'args' => array(
'description' => __( 'Rotation arguments.' ),
'type' => 'object',
'required' => array(
'angle',
),
'properties' => array(
'angle' => array(
'description' => __( 'Angle to rotate clockwise in degrees.' ),
'type' => 'number',
),
),
),
),
),
array(
'title' => __( 'Crop' ),
'properties' => array(
'type' => array(
'description' => __( 'Crop type.' ),
'type' => 'string',
'enum' => array( 'crop' ),
),
'args' => array(
'description' => __( 'Crop arguments.' ),
'type' => 'object',
'required' => array(
'left',
'top',
'width',
'height',
),
'properties' => array(
'left' => array(
'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ),
'type' => 'number',
),
'top' => array(
'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ),
'type' => 'number',
),
'width' => array(
'description' => __( 'Width of the crop as a percentage of the image width.' ),
'type' => 'number',
),
'height' => array(
'description' => __( 'Height of the crop as a percentage of the image height.' ),
'type' => 'number',
),
),
),
),
),
),
),
),
'rotation' => array(
'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ),
'type' => 'integer',
'minimum' => 0,
'exclusiveMinimum' => true,
'maximum' => 360,
'exclusiveMaximum' => true,
),
'x' => array(
'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
'type' => 'number',
'minimum' => 0,
'maximum' => 100,
),
'y' => array(
'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
'type' => 'number',
'minimum' => 0,
'maximum' => 100,
),
'width' => array(
'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
'type' => 'number',
'minimum' => 0,
'maximum' => 100,
),
'height' => array(
'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
'type' => 'number',
'minimum' => 0,
'maximum' => 100,
),
);
}
}
endpoints/class-wp-rest-autosaves-controller.php 0000644 00000034661 15153067731 0016177 0 ustar 00 <?php
/**
* REST API: WP_REST_Autosaves_Controller class.
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core class used to access autosaves via the REST API.
*
* @since 5.0.0
*
* @see WP_REST_Revisions_Controller
* @see WP_REST_Controller
*/
class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {
/**
* Parent post type.
*
* @since 5.0.0
* @var string
*/
private $parent_post_type;
/**
* Parent post controller.
*
* @since 5.0.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* Revision controller.
*
* @since 5.0.0
* @var WP_REST_Revisions_Controller
*/
private $revisions_controller;
/**
* The base of the parent controller's route.
*
* @since 5.0.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 5.0.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
$this->parent_post_type = $parent_post_type;
$post_type_object = get_post_type_object( $parent_post_type );
$parent_controller = $post_type_object->get_rest_controller();
if ( ! $parent_controller ) {
$parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
}
$this->parent_controller = $parent_controller;
$revisions_controller = $post_type_object->get_revisions_rest_controller();
if ( ! $revisions_controller ) {
$revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
}
$this->revisions_controller = $revisions_controller;
$this->rest_base = 'autosaves';
$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
$this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
}
/**
* Registers the routes for autosaves.
*
* @since 5.0.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<id>[\d]+)/' . $this->rest_base,
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the autosave.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the autosave.' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'The ID for the autosave.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get the parent post.
*
* @since 5.0.0
*
* @param int $parent_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent( $parent_id ) {
return $this->revisions_controller->get_parent( $parent_id );
}
/**
* Checks if a given request has access to get autosaves.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$parent = $this->get_parent( $request['id'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
if ( ! current_user_can( 'edit_post', $parent->ID ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to view autosaves of this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to create an autosave revision.
*
* Autosave revisions inherit permissions from the parent post,
* check if the current user has permission to edit the post.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create the item, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
$id = $request->get_param( 'id' );
if ( empty( $id ) ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid item ID.' ),
array( 'status' => 404 )
);
}
return $this->parent_controller->update_item_permissions_check( $request );
}
/**
* Creates, updates or deletes an autosave revision.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) {
define( 'DOING_AUTOSAVE', true );
}
$post = $this->get_parent( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$prepared_post = $this->parent_controller->prepare_item_for_database( $request );
$prepared_post->ID = $post->ID;
$user_id = get_current_user_id();
// We need to check post lock to ensure the original author didn't leave their browser tab open.
if ( ! function_exists( 'wp_check_post_lock' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$post_lock = wp_check_post_lock( $post->ID );
$is_draft = 'draft' === $post->post_status || 'auto-draft' === $post->post_status;
if ( $is_draft && (int) $post->post_author === $user_id && ! $post_lock ) {
/*
* Draft posts for the same author: autosaving updates the post and does not create a revision.
* Convert the post object to an array and add slashes, wp_update_post() expects escaped array.
*/
$autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
} else {
// Non-draft posts: create or update the post autosave. Pass the meta data.
$autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
}
if ( is_wp_error( $autosave_id ) ) {
return $autosave_id;
}
$autosave = get_post( $autosave_id );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $autosave, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Get the autosave, if the ID is valid.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
*/
public function get_item( $request ) {
$parent_id = (int) $request->get_param( 'parent' );
if ( $parent_id <= 0 ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post parent ID.' ),
array( 'status' => 404 )
);
}
$autosave = wp_get_post_autosave( $parent_id );
if ( ! $autosave ) {
return new WP_Error(
'rest_post_no_autosave',
__( 'There is no autosave revision for this post.' ),
array( 'status' => 404 )
);
}
$response = $this->prepare_item_for_response( $autosave, $request );
return $response;
}
/**
* Gets a collection of autosaves using wp_get_post_autosave.
*
* Contains the user's autosave, for empty if it doesn't exist.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$parent = $this->get_parent( $request['id'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
$response = array();
$parent_id = $parent->ID;
$revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) );
foreach ( $revisions as $revision ) {
if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) {
$data = $this->prepare_item_for_response( $revision, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
}
return rest_ensure_response( $response );
}
/**
* Retrieves the autosave's schema, conforming to JSON Schema.
*
* @since 5.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = $this->revisions_controller->get_item_schema();
$schema['properties']['preview_link'] = array(
'description' => __( 'Preview link for the post.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'edit' ),
'readonly' => true,
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Creates autosave for the specified post.
*
* From wp-admin/post.php.
*
* @since 5.0.0
* @since 6.4.0 The `$meta` parameter was added.
*
* @param array $post_data Associative array containing the post data.
* @param array $meta Associative array containing the post meta data.
* @return mixed The autosave revision ID or WP_Error.
*/
public function create_post_autosave( $post_data, array $meta = array() ) {
$post_id = (int) $post_data['ID'];
$post = get_post( $post_id );
if ( is_wp_error( $post ) ) {
return $post;
}
// Only create an autosave when it is different from the saved post.
$autosave_is_different = false;
$new_autosave = _wp_post_revision_data( $post_data, true );
foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) {
if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) {
$autosave_is_different = true;
break;
}
}
// Check if meta values have changed.
if ( ! empty( $meta ) ) {
$revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type );
foreach ( $revisioned_meta_keys as $meta_key ) {
// get_metadata_raw is used to avoid retrieving the default value.
$old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true );
$new_meta = isset( $meta[ $meta_key ] ) ? $meta[ $meta_key ] : '';
if ( $new_meta !== $old_meta ) {
$autosave_is_different = true;
break;
}
}
}
$user_id = get_current_user_id();
// Store one autosave per author. If there is already an autosave, overwrite it.
$old_autosave = wp_get_post_autosave( $post_id, $user_id );
if ( ! $autosave_is_different && $old_autosave ) {
// Nothing to save, return the existing autosave.
return $old_autosave->ID;
}
if ( $old_autosave ) {
$new_autosave['ID'] = $old_autosave->ID;
$new_autosave['post_author'] = $user_id;
/** This filter is documented in wp-admin/post.php */
do_action( 'wp_creating_autosave', $new_autosave );
// wp_update_post() expects escaped array.
$revision_id = wp_update_post( wp_slash( $new_autosave ) );
} else {
// Create the new autosave as a special post revision.
$revision_id = _wp_put_post_revision( $post_data, true );
}
if ( is_wp_error( $revision_id ) || 0 === $revision_id ) {
return $revision_id;
}
// Attached any passed meta values that have revisions enabled.
if ( ! empty( $meta ) ) {
foreach ( $revisioned_meta_keys as $meta_key ) {
if ( isset( $meta[ $meta_key ] ) ) {
update_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta[ $meta_key ] ) );
}
}
}
return $revision_id;
}
/**
* Prepares the revision for the REST response.
*
* @since 5.0.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post $item Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$response = $this->revisions_controller->prepare_item_for_response( $post, $request );
$fields = $this->get_fields_for_response( $request );
if ( in_array( 'preview_link', $fields, true ) ) {
$parent_id = wp_is_post_autosave( $post );
$preview_post_id = false === $parent_id ? $post->ID : $parent_id;
$preview_query_args = array();
if ( false !== $parent_id ) {
$preview_query_args['preview_id'] = $parent_id;
$preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id );
}
$response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$response->data = $this->add_additional_fields_to_object( $response->data, $request );
$response->data = $this->filter_response_by_context( $response->data, $context );
/**
* Filters a revision returned from the REST API.
*
* Allows modification of the revision right before it is returned.
*
* @since 5.0.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The original revision object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_autosave', $response, $post, $request );
}
/**
* Retrieves the query params for the autosaves collection.
*
* @since 5.0.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
endpoints/class-wp-rest-block-directory-controller.php 0000644 00000023340 15153067731 0017251 0 ustar 00 <?php
/**
* REST API: WP_REST_Block_Directory_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.5.0
*/
/**
* Controller which provides REST endpoint for the blocks.
*
* @since 5.5.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Directory_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-directory';
}
/**
* Registers the necessary REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/search',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to install and activate plugins.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has permission, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_block_directory_cannot_view',
__( 'Sorry, you are not allowed to browse the block directory.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Search and retrieve blocks metadata
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$response = plugins_api(
'query_plugins',
array(
'block' => $request['term'],
'per_page' => $request['per_page'],
'page' => $request['page'],
)
);
if ( is_wp_error( $response ) ) {
$response->add_data( array( 'status' => 500 ) );
return $response;
}
$result = array();
foreach ( $response->plugins as $plugin ) {
// If the API returned a plugin with empty data for 'blocks', skip it.
if ( empty( $plugin['blocks'] ) ) {
continue;
}
$data = $this->prepare_item_for_response( $plugin, $request );
$result[] = $this->prepare_response_for_collection( $data );
}
return rest_ensure_response( $result );
}
/**
* Parse block metadata for a block, and prepare it for an API response.
*
* @since 5.5.0
* @since 5.9.0 Renamed `$plugin` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param array $item The plugin metadata.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$plugin = $item;
$fields = $this->get_fields_for_response( $request );
// There might be multiple blocks in a plugin. Only the first block is mapped.
$block_data = reset( $plugin['blocks'] );
// A data array containing the properties we'll return.
$block = array(
'name' => $block_data['name'],
'title' => ( $block_data['title'] ? $block_data['title'] : $plugin['name'] ),
'description' => wp_trim_words( $plugin['short_description'], 30, '...' ),
'id' => $plugin['slug'],
'rating' => $plugin['rating'] / 20,
'rating_count' => (int) $plugin['num_ratings'],
'active_installs' => (int) $plugin['active_installs'],
'author_block_rating' => $plugin['author_block_rating'] / 20,
'author_block_count' => (int) $plugin['author_block_count'],
'author' => wp_strip_all_tags( $plugin['author'] ),
'icon' => ( isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default' ),
'last_updated' => gmdate( 'Y-m-d\TH:i:s', strtotime( $plugin['last_updated'] ) ),
'humanized_updated' => sprintf(
/* translators: %s: Human-readable time difference. */
__( '%s ago' ),
human_time_diff( strtotime( $plugin['last_updated'] ) )
),
);
$this->add_additional_fields_to_object( $block, $request );
$response = new WP_REST_Response( $block );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $plugin ) );
}
return $response;
}
/**
* Generates a list of links to include in the response for the plugin.
*
* @since 5.5.0
*
* @param array $plugin The plugin data from WordPress.org.
* @return array
*/
protected function prepare_links( $plugin ) {
$links = array(
'https://api.w.org/install-plugin' => array(
'href' => add_query_arg( 'slug', urlencode( $plugin['slug'] ), rest_url( 'wp/v2/plugins' ) ),
),
);
$plugin_file = $this->find_plugin_for_slug( $plugin['slug'] );
if ( $plugin_file ) {
$links['https://api.w.org/plugin'] = array(
'href' => rest_url( 'wp/v2/plugins/' . substr( $plugin_file, 0, - 4 ) ),
'embeddable' => true,
);
}
return $links;
}
/**
* Finds an installed plugin for the given slug.
*
* @since 5.5.0
*
* @param string $slug The WordPress.org directory slug for a plugin.
* @return string The plugin file found matching it.
*/
protected function find_plugin_for_slug( $slug ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$plugin_files = get_plugins( '/' . $slug );
if ( ! $plugin_files ) {
return '';
}
$plugin_files = array_keys( $plugin_files );
return $slug . '/' . reset( $plugin_files );
}
/**
* Retrieves the theme's schema, conforming to JSON Schema.
*
* @since 5.5.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-directory-item',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The block name, in namespace/block-name format.' ),
'type' => 'string',
'context' => array( 'view' ),
),
'title' => array(
'description' => __( 'The block title, in human readable format.' ),
'type' => 'string',
'context' => array( 'view' ),
),
'description' => array(
'description' => __( 'A short description of the block, in human readable format.' ),
'type' => 'string',
'context' => array( 'view' ),
),
'id' => array(
'description' => __( 'The block slug.' ),
'type' => 'string',
'context' => array( 'view' ),
),
'rating' => array(
'description' => __( 'The star rating of the block.' ),
'type' => 'number',
'context' => array( 'view' ),
),
'rating_count' => array(
'description' => __( 'The number of ratings.' ),
'type' => 'integer',
'context' => array( 'view' ),
),
'active_installs' => array(
'description' => __( 'The number sites that have activated this block.' ),
'type' => 'integer',
'context' => array( 'view' ),
),
'author_block_rating' => array(
'description' => __( 'The average rating of blocks published by the same author.' ),
'type' => 'number',
'context' => array( 'view' ),
),
'author_block_count' => array(
'description' => __( 'The number of blocks published by the same author.' ),
'type' => 'integer',
'context' => array( 'view' ),
),
'author' => array(
'description' => __( 'The WordPress.org username of the block author.' ),
'type' => 'string',
'context' => array( 'view' ),
),
'icon' => array(
'description' => __( 'The block icon.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
),
'last_updated' => array(
'description' => __( 'The date when the block was last updated.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view' ),
),
'humanized_updated' => array(
'description' => __( 'The date when the block was last updated, in fuzzy human readable format.' ),
'type' => 'string',
'context' => array( 'view' ),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the search params for the blocks collection.
*
* @since 5.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['term'] = array(
'description' => __( 'Limit result set to blocks matching the search term.' ),
'type' => 'string',
'required' => true,
'minLength' => 1,
);
unset( $query_params['search'] );
/**
* Filters REST API collection parameters for the block directory controller.
*
* @since 5.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_block_directory_collection_params', $query_params );
}
}
endpoints/class-wp-rest-block-pattern-categories-controller.php 0000644 00000011060 15153067731 0021041 0 ustar 00 <?php
/**
* REST API: WP_REST_Block_Pattern_Categories_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.0.0
*/
/**
* Core class used to access block pattern categories via the REST API.
*
* @since 6.0.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Pattern_Categories_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*
* @since 6.0.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-patterns/categories';
}
/**
* Registers the routes for the objects of the controller.
*
* @since 6.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read block patterns.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view the registered block pattern categories.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Retrieves all block pattern categories.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$response = array();
$categories = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered();
foreach ( $categories as $category ) {
$prepared_category = $this->prepare_item_for_response( $category, $request );
$response[] = $this->prepare_response_for_collection( $prepared_category );
}
return rest_ensure_response( $response );
}
/**
* Prepare a raw block pattern category before it gets output in a REST API response.
*
* @since 6.0.0
*
* @param array $item Raw category as registered, before any changes.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$keys = array( 'name', 'label', 'description' );
$data = array();
foreach ( $keys as $key ) {
if ( isset( $item[ $key ] ) && rest_is_field_included( $key, $fields ) ) {
$data[ $key ] = $item[ $key ];
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
return rest_ensure_response( $data );
}
/**
* Retrieves the block pattern category schema, conforming to JSON Schema.
*
* @since 6.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-pattern-category',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The category name.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'label' => array(
'description' => __( 'The category label, in human readable format.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => __( 'The category description, in human readable format.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-block-patterns-controller.php 0000644 00000021471 15153067731 0017110 0 ustar 00 <?php
/**
* REST API: WP_REST_Block_Patterns_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.0.0
*/
/**
* Core class used to access block patterns via the REST API.
*
* @since 6.0.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Patterns_Controller extends WP_REST_Controller {
/**
* Defines whether remote patterns should be loaded.
*
* @since 6.0.0
* @var bool
*/
private $remote_patterns_loaded;
/**
* An array that maps old categories names to new ones.
*
* @since 6.2.0
* @var array
*/
protected static $categories_migration = array(
'buttons' => 'call-to-action',
'columns' => 'text',
'query' => 'posts',
);
/**
* Constructs the controller.
*
* @since 6.0.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-patterns/patterns';
}
/**
* Registers the routes for the objects of the controller.
*
* @since 6.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read block patterns.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view the registered block patterns.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Retrieves all block patterns.
*
* @since 6.0.0
* @since 6.2.0 Added migration for old core pattern categories to the new ones.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
if ( ! $this->remote_patterns_loaded ) {
// Load block patterns from w.org.
_load_remote_block_patterns(); // Patterns with the `core` keyword.
_load_remote_featured_patterns(); // Patterns in the `featured` category.
_register_remote_theme_patterns(); // Patterns requested by current theme.
$this->remote_patterns_loaded = true;
}
$response = array();
$patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
foreach ( $patterns as $pattern ) {
$migrated_pattern = $this->migrate_pattern_categories( $pattern );
$prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request );
$response[] = $this->prepare_response_for_collection( $prepared_pattern );
}
return rest_ensure_response( $response );
}
/**
* Migrates old core pattern categories to the new categories.
*
* Core pattern categories are revamped. Migration is needed to ensure
* backwards compatibility.
*
* @since 6.2.0
*
* @param array $pattern Raw pattern as registered, before applying any changes.
* @return array Migrated pattern.
*/
protected function migrate_pattern_categories( $pattern ) {
// No categories to migrate.
if (
! isset( $pattern['categories'] ) ||
! is_array( $pattern['categories'] )
) {
return $pattern;
}
foreach ( $pattern['categories'] as $index => $category ) {
// If the category exists as a key, then it needs migration.
if ( isset( static::$categories_migration[ $category ] ) ) {
$pattern['categories'][ $index ] = static::$categories_migration[ $category ];
}
}
return $pattern;
}
/**
* Prepare a raw block pattern before it gets output in a REST API response.
*
* @since 6.0.0
* @since 6.3.0 Added `source` property.
*
* @param array $item Raw pattern as registered, before any changes.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$keys = array(
'name' => 'name',
'title' => 'title',
'content' => 'content',
'description' => 'description',
'viewportWidth' => 'viewport_width',
'inserter' => 'inserter',
'categories' => 'categories',
'keywords' => 'keywords',
'blockTypes' => 'block_types',
'postTypes' => 'post_types',
'templateTypes' => 'template_types',
'source' => 'source',
);
$data = array();
foreach ( $keys as $item_key => $rest_key ) {
if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) {
$data[ $rest_key ] = $item[ $item_key ];
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
return rest_ensure_response( $data );
}
/**
* Retrieves the block pattern schema, conforming to JSON Schema.
*
* @since 6.0.0
* @since 6.3.0 Added `source` property.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-pattern',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The pattern name.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'title' => array(
'description' => __( 'The pattern title, in human readable format.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'content' => array(
'description' => __( 'The pattern content.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => __( 'The pattern detailed description.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'viewport_width' => array(
'description' => __( 'The pattern viewport width for inserter preview.' ),
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'inserter' => array(
'description' => __( 'Determines whether the pattern is visible in inserter.' ),
'type' => 'boolean',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'categories' => array(
'description' => __( 'The pattern category slugs.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'keywords' => array(
'description' => __( 'The pattern keywords.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'block_types' => array(
'description' => __( 'Block types that the pattern is intended to be used with.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'post_types' => array(
'description' => __( 'An array of post types that the pattern is restricted to be used with.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'template_types' => array(
'description' => __( 'An array of template types where the pattern fits.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'source' => array(
'description' => __( 'Where the pattern comes from e.g. core' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
'enum' => array(
'core',
'plugin',
'theme',
'pattern-directory/core',
'pattern-directory/theme',
'pattern-directory/featured',
),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-block-renderer-controller.php 0000644 00000013312 15153067731 0017051 0 ustar 00 <?php
/**
* Block Renderer REST API: WP_REST_Block_Renderer_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Controller which provides REST endpoint for rendering a block.
*
* @since 5.0.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Renderer_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*
* @since 5.0.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-renderer';
}
/**
* Registers the necessary REST API routes, one for each dynamic block.
*
* @since 5.0.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<name>[a-z0-9-]+/[a-z0-9-]+)',
array(
'args' => array(
'name' => array(
'description' => __( 'Unique registered name for the block.' ),
'type' => 'string',
),
),
array(
'methods' => array( WP_REST_Server::READABLE, WP_REST_Server::CREATABLE ),
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'attributes' => array(
'description' => __( 'Attributes for the block.' ),
'type' => 'object',
'default' => array(),
'validate_callback' => static function ( $value, $request ) {
$block = WP_Block_Type_Registry::get_instance()->get_registered( $request['name'] );
if ( ! $block ) {
// This will get rejected in ::get_item().
return true;
}
$schema = array(
'type' => 'object',
'properties' => $block->get_attributes(),
'additionalProperties' => false,
);
return rest_validate_value_from_schema( $value, $schema );
},
'sanitize_callback' => static function ( $value, $request ) {
$block = WP_Block_Type_Registry::get_instance()->get_registered( $request['name'] );
if ( ! $block ) {
// This will get rejected in ::get_item().
return true;
}
$schema = array(
'type' => 'object',
'properties' => $block->get_attributes(),
'additionalProperties' => false,
);
return rest_sanitize_value_from_schema( $value, $schema );
},
),
'post_id' => array(
'description' => __( 'ID of the post context.' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read blocks.
*
* @since 5.0.0
*
* @global WP_Post $post Global post object.
*
* @param WP_REST_Request $request Request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
global $post;
$post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;
if ( $post_id > 0 ) {
$post = get_post( $post_id );
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error(
'block_cannot_read',
__( 'Sorry, you are not allowed to read blocks of this post.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
} else {
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'block_cannot_read',
__( 'Sorry, you are not allowed to read blocks as this user.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
}
return true;
}
/**
* Returns block output from block's registered render_callback.
*
* @since 5.0.0
*
* @global WP_Post $post Global post object.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
global $post;
$post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;
if ( $post_id > 0 ) {
$post = get_post( $post_id );
// Set up postdata since this will be needed if post_id was set.
setup_postdata( $post );
}
$registry = WP_Block_Type_Registry::get_instance();
$registered = $registry->get_registered( $request['name'] );
if ( null === $registered || ! $registered->is_dynamic() ) {
return new WP_Error(
'block_invalid',
__( 'Invalid block.' ),
array(
'status' => 404,
)
);
}
$attributes = $request->get_param( 'attributes' );
// Create an array representation simulating the output of parse_blocks.
$block = array(
'blockName' => $request['name'],
'attrs' => $attributes,
'innerHTML' => '',
'innerContent' => array(),
);
// Render using render_block to ensure all relevant filters are used.
$data = array(
'rendered' => render_block( $block ),
);
return rest_ensure_response( $data );
}
/**
* Retrieves block's output schema, conforming to JSON Schema.
*
* @since 5.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->schema;
}
$this->schema = array(
'$schema' => 'http://json-schema.org/schema#',
'title' => 'rendered-block',
'type' => 'object',
'properties' => array(
'rendered' => array(
'description' => __( 'The rendered block.' ),
'type' => 'string',
'required' => true,
'context' => array( 'edit' ),
),
),
);
return $this->schema;
}
}
endpoints/class-wp-rest-block-types-controller.php 0000644 00000063445 15153067731 0016423 0 ustar 00 <?php
/**
* REST API: WP_REST_Block_Types_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.5.0
*/
/**
* Core class used to access block types via the REST API.
*
* @since 5.5.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Types_Controller extends WP_REST_Controller {
const NAME_PATTERN = '^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$';
/**
* Instance of WP_Block_Type_Registry.
*
* @since 5.5.0
* @var WP_Block_Type_Registry
*/
protected $block_registry;
/**
* Instance of WP_Block_Styles_Registry.
*
* @since 5.5.0
* @var WP_Block_Styles_Registry
*/
protected $style_registry;
/**
* Constructor.
*
* @since 5.5.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-types';
$this->block_registry = WP_Block_Type_Registry::get_instance();
$this->style_registry = WP_Block_Styles_Registry::get_instance();
}
/**
* Registers the routes for block types.
*
* @since 5.5.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<namespace>[a-zA-Z0-9_-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<namespace>[a-zA-Z0-9_-]+)/(?P<name>[a-zA-Z0-9_-]+)',
array(
'args' => array(
'name' => array(
'description' => __( 'Block name.' ),
'type' => 'string',
),
'namespace' => array(
'description' => __( 'Block namespace.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read post block types.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
return $this->check_read_permission();
}
/**
* Retrieves all post block types, depending on user context.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$data = array();
$block_types = $this->block_registry->get_all_registered();
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
$namespace = '';
if ( isset( $registered['namespace'] ) && ! empty( $request['namespace'] ) ) {
$namespace = $request['namespace'];
}
foreach ( $block_types as $slug => $obj ) {
if ( $namespace ) {
list ( $block_namespace ) = explode( '/', $obj->name );
if ( $namespace !== $block_namespace ) {
continue;
}
}
$block_type = $this->prepare_item_for_response( $obj, $request );
$data[] = $this->prepare_response_for_collection( $block_type );
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read a block type.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$check = $this->check_read_permission();
if ( is_wp_error( $check ) ) {
return $check;
}
$block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] );
$block_type = $this->get_block( $block_name );
if ( is_wp_error( $block_type ) ) {
return $block_type;
}
return true;
}
/**
* Checks whether a given block type should be visible.
*
* @since 5.5.0
*
* @return true|WP_Error True if the block type is visible, WP_Error otherwise.
*/
protected function check_read_permission() {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error( 'rest_block_type_cannot_view', __( 'Sorry, you are not allowed to manage block types.' ), array( 'status' => rest_authorization_required_code() ) );
}
/**
* Get the block, if the name is valid.
*
* @since 5.5.0
*
* @param string $name Block name.
* @return WP_Block_Type|WP_Error Block type object if name is valid, WP_Error otherwise.
*/
protected function get_block( $name ) {
$block_type = $this->block_registry->get_registered( $name );
if ( empty( $block_type ) ) {
return new WP_Error( 'rest_block_type_invalid', __( 'Invalid block type.' ), array( 'status' => 404 ) );
}
return $block_type;
}
/**
* Retrieves a specific block type.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] );
$block_type = $this->get_block( $block_name );
if ( is_wp_error( $block_type ) ) {
return $block_type;
}
$data = $this->prepare_item_for_response( $block_type, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a block type object for serialization.
*
* @since 5.5.0
* @since 5.9.0 Renamed `$block_type` to `$item` to match parent class for PHP 8 named parameter support.
* @since 6.3.0 Added `selectors` field.
* @since 6.5.0 Added `view_script_module_ids` field.
*
* @param WP_Block_Type $item Block type data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Block type data.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$block_type = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'attributes', $fields ) ) {
$data['attributes'] = $block_type->get_attributes();
}
if ( rest_is_field_included( 'is_dynamic', $fields ) ) {
$data['is_dynamic'] = $block_type->is_dynamic();
}
$schema = $this->get_item_schema();
// Fields deprecated in WordPress 6.1, but left in the schema for backwards compatibility.
$deprecated_fields = array(
'editor_script',
'script',
'view_script',
'editor_style',
'style',
);
$extra_fields = array_merge(
array(
'api_version',
'name',
'title',
'description',
'icon',
'category',
'keywords',
'parent',
'ancestor',
'allowed_blocks',
'provides_context',
'uses_context',
'selectors',
'supports',
'styles',
'textdomain',
'example',
'editor_script_handles',
'script_handles',
'view_script_handles',
'view_script_module_ids',
'editor_style_handles',
'style_handles',
'view_style_handles',
'variations',
'block_hooks',
),
$deprecated_fields
);
foreach ( $extra_fields as $extra_field ) {
if ( rest_is_field_included( $extra_field, $fields ) ) {
if ( isset( $block_type->$extra_field ) ) {
$field = $block_type->$extra_field;
if ( in_array( $extra_field, $deprecated_fields, true ) && is_array( $field ) ) {
// Since the schema only allows strings or null (but no arrays), we return the first array item.
$field = ! empty( $field ) ? array_shift( $field ) : '';
}
} elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
$field = $schema['properties'][ $extra_field ]['default'];
} else {
$field = '';
}
$data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
}
}
if ( rest_is_field_included( 'styles', $fields ) ) {
$styles = $this->style_registry->get_registered_styles_for_block( $block_type->name );
$styles = array_values( $styles );
$data['styles'] = wp_parse_args( $styles, $data['styles'] );
$data['styles'] = array_filter( $data['styles'] );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $block_type ) );
}
/**
* Filters a block type returned from the REST API.
*
* Allows modification of the block type data right before it is returned.
*
* @since 5.5.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Block_Type $block_type The original block type object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_block_type', $response, $block_type, $request );
}
/**
* Prepares links for the request.
*
* @since 5.5.0
*
* @param WP_Block_Type $block_type Block type data.
* @return array Links for the given block type.
*/
protected function prepare_links( $block_type ) {
list( $namespace ) = explode( '/', $block_type->name );
$links = array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $block_type->name ) ),
),
'up' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $namespace ) ),
),
);
if ( $block_type->is_dynamic() ) {
$links['https://api.w.org/render-block'] = array(
'href' => add_query_arg(
'context',
'edit',
rest_url( sprintf( '%s/%s/%s', 'wp/v2', 'block-renderer', $block_type->name ) )
),
);
}
return $links;
}
/**
* Retrieves the block type' schema, conforming to JSON Schema.
*
* @since 5.5.0
* @since 6.3.0 Added `selectors` field.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
// rest_validate_value_from_schema doesn't understand $refs, pull out reused definitions for readability.
$inner_blocks_definition = array(
'description' => __( 'The list of inner blocks used in the example.' ),
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The name of the inner block.' ),
'type' => 'string',
'pattern' => self::NAME_PATTERN,
'required' => true,
),
'attributes' => array(
'description' => __( 'The attributes of the inner block.' ),
'type' => 'object',
),
'innerBlocks' => array(
'description' => __( "A list of the inner block's own inner blocks. This is a recursive definition following the parent innerBlocks schema." ),
'type' => 'array',
),
),
),
);
$example_definition = array(
'description' => __( 'Block example.' ),
'type' => array( 'object', 'null' ),
'default' => null,
'properties' => array(
'attributes' => array(
'description' => __( 'The attributes used in the example.' ),
'type' => 'object',
),
'innerBlocks' => $inner_blocks_definition,
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$keywords_definition = array(
'description' => __( 'Block keywords.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$icon_definition = array(
'description' => __( 'Icon of block type.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$category_definition = array(
'description' => __( 'Block category.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-type',
'type' => 'object',
'properties' => array(
'api_version' => array(
'description' => __( 'Version of block API.' ),
'type' => 'integer',
'default' => 1,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Title of block type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Unique name identifying the block type.' ),
'type' => 'string',
'pattern' => self::NAME_PATTERN,
'required' => true,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of block type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'icon' => $icon_definition,
'attributes' => array(
'description' => __( 'Block attributes.' ),
'type' => array( 'object', 'null' ),
'properties' => array(),
'default' => null,
'additionalProperties' => array(
'type' => 'object',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'provides_context' => array(
'description' => __( 'Context provided by blocks of this type.' ),
'type' => 'object',
'properties' => array(),
'additionalProperties' => array(
'type' => 'string',
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'uses_context' => array(
'description' => __( 'Context values inherited by blocks of this type.' ),
'type' => 'array',
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'selectors' => array(
'description' => __( 'Custom CSS selectors.' ),
'type' => 'object',
'default' => array(),
'properties' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'supports' => array(
'description' => __( 'Block supports.' ),
'type' => 'object',
'default' => array(),
'properties' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'category' => $category_definition,
'is_dynamic' => array(
'description' => __( 'Is the block dynamically rendered.' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'editor_script_handles' => array(
'description' => __( 'Editor script handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'script_handles' => array(
'description' => __( 'Public facing and editor script handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'view_script_handles' => array(
'description' => __( 'Public facing script handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'view_script_module_ids' => array(
'description' => __( 'Public facing script module IDs.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'editor_style_handles' => array(
'description' => __( 'Editor style handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'style_handles' => array(
'description' => __( 'Public facing and editor style handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'view_style_handles' => array(
'description' => __( 'Public facing style handles.' ),
'type' => array( 'array' ),
'default' => array(),
'items' => array(
'type' => 'string',
),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'styles' => array(
'description' => __( 'Block style variations.' ),
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Unique name identifying the style.' ),
'type' => 'string',
'required' => true,
),
'label' => array(
'description' => __( 'The human-readable label for the style.' ),
'type' => 'string',
),
'inline_style' => array(
'description' => __( 'Inline CSS code that registers the CSS class required for the style.' ),
'type' => 'string',
),
'style_handle' => array(
'description' => __( 'Contains the handle that defines the block style.' ),
'type' => 'string',
),
),
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'variations' => array(
'description' => __( 'Block variations.' ),
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The unique and machine-readable name.' ),
'type' => 'string',
'required' => true,
),
'title' => array(
'description' => __( 'A human-readable variation title.' ),
'type' => 'string',
'required' => true,
),
'description' => array(
'description' => __( 'A detailed variation description.' ),
'type' => 'string',
'required' => false,
),
'category' => $category_definition,
'icon' => $icon_definition,
'isDefault' => array(
'description' => __( 'Indicates whether the current variation is the default one.' ),
'type' => 'boolean',
'required' => false,
'default' => false,
),
'attributes' => array(
'description' => __( 'The initial values for attributes.' ),
'type' => 'object',
),
'innerBlocks' => $inner_blocks_definition,
'example' => $example_definition,
'scope' => array(
'description' => __( 'The list of scopes where the variation is applicable. When not provided, it assumes all available scopes.' ),
'type' => array( 'array', 'null' ),
'default' => null,
'items' => array(
'type' => 'string',
'enum' => array( 'block', 'inserter', 'transform' ),
),
'readonly' => true,
),
'keywords' => $keywords_definition,
),
),
'readonly' => true,
'context' => array( 'embed', 'view', 'edit' ),
'default' => null,
),
'textdomain' => array(
'description' => __( 'Public text domain.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'parent' => array(
'description' => __( 'Parent blocks.' ),
'type' => array( 'array', 'null' ),
'items' => array(
'type' => 'string',
'pattern' => self::NAME_PATTERN,
),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'ancestor' => array(
'description' => __( 'Ancestor blocks.' ),
'type' => array( 'array', 'null' ),
'items' => array(
'type' => 'string',
'pattern' => self::NAME_PATTERN,
),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'allowed_blocks' => array(
'description' => __( 'Allowed child block types.' ),
'type' => array( 'array', 'null' ),
'items' => array(
'type' => 'string',
'pattern' => self::NAME_PATTERN,
),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'keywords' => $keywords_definition,
'example' => $example_definition,
'block_hooks' => array(
'description' => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.' ),
'type' => 'object',
'patternProperties' => array(
self::NAME_PATTERN => array(
'type' => 'string',
'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
),
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
),
);
// Properties deprecated in WordPress 6.1, but left in the schema for backwards compatibility.
$deprecated_properties = array(
'editor_script' => array(
'description' => __( 'Editor script handle. DEPRECATED: Use `editor_script_handles` instead.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'script' => array(
'description' => __( 'Public facing and editor script handle. DEPRECATED: Use `script_handles` instead.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'view_script' => array(
'description' => __( 'Public facing script handle. DEPRECATED: Use `view_script_handles` instead.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'editor_style' => array(
'description' => __( 'Editor style handle. DEPRECATED: Use `editor_style_handles` instead.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'style' => array(
'description' => __( 'Public facing and editor style handle. DEPRECATED: Use `style_handles` instead.' ),
'type' => array( 'string', 'null' ),
'default' => null,
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
);
$this->schema['properties'] = array_merge( $this->schema['properties'], $deprecated_properties );
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 5.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'namespace' => array(
'description' => __( 'Block namespace.' ),
'type' => 'string',
),
);
}
}
endpoints/class-wp-rest-blocks-controller.php 0000644 00000006152 15153067731 0015434 0 ustar 00 <?php
/**
* Synced patterns REST API: WP_REST_Blocks_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Controller which provides a REST endpoint for the editor to read, create,
* edit, and delete synced patterns (formerly called reusable blocks).
* Patterns are stored as posts with the wp_block post type.
*
* @since 5.0.0
*
* @see WP_REST_Posts_Controller
* @see WP_REST_Controller
*/
class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller {
/**
* Checks if a pattern can be read.
*
* @since 5.0.0
*
* @param WP_Post $post Post object that backs the block.
* @return bool Whether the pattern can be read.
*/
public function check_read_permission( $post ) {
// By default the read_post capability is mapped to edit_posts.
if ( ! current_user_can( 'read_post', $post->ID ) ) {
return false;
}
return parent::check_read_permission( $post );
}
/**
* Filters a response based on the context defined in the schema.
*
* @since 5.0.0
* @since 6.3.0 Adds the `wp_pattern_sync_status` postmeta property to the top level of response.
*
* @param array $data Response data to filter.
* @param string $context Context defined in the schema.
* @return array Filtered response.
*/
public function filter_response_by_context( $data, $context ) {
$data = parent::filter_response_by_context( $data, $context );
/*
* Remove `title.rendered` and `content.rendered` from the response.
* It doesn't make sense for a pattern to have rendered content on its own,
* since rendering a block requires it to be inside a post or a page.
*/
unset( $data['title']['rendered'] );
unset( $data['content']['rendered'] );
// Add the core wp_pattern_sync_status meta as top level property to the response.
$data['wp_pattern_sync_status'] = isset( $data['meta']['wp_pattern_sync_status'] ) ? $data['meta']['wp_pattern_sync_status'] : '';
unset( $data['meta']['wp_pattern_sync_status'] );
return $data;
}
/**
* Retrieves the pattern's schema, conforming to JSON Schema.
*
* @since 5.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = parent::get_item_schema();
/*
* Allow all contexts to access `title.raw` and `content.raw`.
* Clients always need the raw markup of a pattern to do anything useful,
* e.g. parse it or display it in an editor.
*/
$schema['properties']['title']['properties']['raw']['context'] = array( 'view', 'edit' );
$schema['properties']['content']['properties']['raw']['context'] = array( 'view', 'edit' );
/*
* Remove `title.rendered` and `content.rendered` from the schema.
* It doesn't make sense for a pattern to have rendered content on its own,
* since rendering a block requires it to be inside a post or a page.
*/
unset( $schema['properties']['title']['properties']['rendered'] );
unset( $schema['properties']['content']['properties']['rendered'] );
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-comments-controller.php 0000644 00000160372 15153067731 0016011 0 ustar 00 <?php
/**
* REST API: WP_REST_Comments_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core controller used to access comments via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Comments_Controller extends WP_REST_Controller {
/**
* Instance of a comment meta fields object.
*
* @since 4.7.0
* @var WP_REST_Comment_Meta_Fields
*/
protected $meta;
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'comments';
$this->meta = new WP_REST_Comment_Meta_Fields();
}
/**
* Registers the routes for comments.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the comment.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'password' => array(
'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
'type' => 'string',
),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass Trash and force deletion.' ),
),
'password' => array(
'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read comments.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( ! empty( $request['post'] ) ) {
foreach ( (array) $request['post'] as $post_id ) {
$post = get_post( $post_id );
if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) {
return new WP_Error(
'rest_cannot_read_post',
__( 'Sorry, you are not allowed to read the post for this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
} elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to read comments without a post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
}
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit comments.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! current_user_can( 'edit_posts' ) ) {
$protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
$forbidden_params = array();
foreach ( $protected_params as $param ) {
if ( 'status' === $param ) {
if ( 'approve' !== $request[ $param ] ) {
$forbidden_params[] = $param;
}
} elseif ( 'type' === $param ) {
if ( 'comment' !== $request[ $param ] ) {
$forbidden_params[] = $param;
}
} elseif ( ! empty( $request[ $param ] ) ) {
$forbidden_params[] = $param;
}
}
if ( ! empty( $forbidden_params ) ) {
return new WP_Error(
'rest_forbidden_param',
/* translators: %s: List of forbidden parameters. */
sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ),
array( 'status' => rest_authorization_required_code() )
);
}
}
return true;
}
/**
* Retrieves a list of comment items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'author' => 'author__in',
'author_email' => 'author_email',
'author_exclude' => 'author__not_in',
'exclude' => 'comment__not_in',
'include' => 'comment__in',
'offset' => 'offset',
'order' => 'order',
'parent' => 'parent__in',
'parent_exclude' => 'parent__not_in',
'per_page' => 'number',
'post' => 'post__in',
'search' => 'search',
'status' => 'status',
'type' => 'type',
);
$prepared_args = array();
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $prepared_args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
// Ensure certain parameter values default to empty strings.
foreach ( array( 'author_email', 'search' ) as $param ) {
if ( ! isset( $prepared_args[ $param ] ) ) {
$prepared_args[ $param ] = '';
}
}
if ( isset( $registered['orderby'] ) ) {
$prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
}
$prepared_args['no_found_rows'] = false;
$prepared_args['update_comment_post_cache'] = true;
$prepared_args['date_query'] = array();
// Set before into date query. Date query must be specified as an array of an array.
if ( isset( $registered['before'], $request['before'] ) ) {
$prepared_args['date_query'][0]['before'] = $request['before'];
}
// Set after into date query. Date query must be specified as an array of an array.
if ( isset( $registered['after'], $request['after'] ) ) {
$prepared_args['date_query'][0]['after'] = $request['after'];
}
if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
$prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
}
/**
* Filters WP_Comment_Query arguments when querying comments via the REST API.
*
* @since 4.7.0
*
* @link https://developer.wordpress.org/reference/classes/wp_comment_query/
*
* @param array $prepared_args Array of arguments for WP_Comment_Query.
* @param WP_REST_Request $request The REST API request.
*/
$prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );
$query = new WP_Comment_Query();
$query_result = $query->query( $prepared_args );
$comments = array();
foreach ( $query_result as $comment ) {
if ( ! $this->check_read_permission( $comment, $request ) ) {
continue;
}
$data = $this->prepare_item_for_response( $comment, $request );
$comments[] = $this->prepare_response_for_collection( $data );
}
$total_comments = (int) $query->found_comments;
$max_pages = (int) $query->max_num_pages;
if ( $total_comments < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $prepared_args['number'], $prepared_args['offset'] );
$query = new WP_Comment_Query();
$prepared_args['count'] = true;
$prepared_args['orderby'] = 'none';
$total_comments = $query->query( $prepared_args );
$max_pages = (int) ceil( $total_comments / $request['per_page'] );
}
$response = rest_ensure_response( $comments );
$response->header( 'X-WP-Total', $total_comments );
$response->header( 'X-WP-TotalPages', $max_pages );
$base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $request['page'] > 1 ) {
$prev_page = $request['page'] - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $request['page'] ) {
$next_page = $request['page'] + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Get the comment, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
*/
protected function get_comment( $id ) {
$error = new WP_Error(
'rest_comment_invalid_id',
__( 'Invalid comment ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$id = (int) $id;
$comment = get_comment( $id );
if ( empty( $comment ) ) {
return $error;
}
if ( ! empty( $comment->comment_post_ID ) ) {
$post = get_post( (int) $comment->comment_post_ID );
if ( empty( $post ) ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post ID.' ),
array( 'status' => 404 )
);
}
}
return $comment;
}
/**
* Checks if a given request has access to read the comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit comments.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$post = get_post( $comment->comment_post_ID );
if ( ! $this->check_read_permission( $comment, $request ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to read this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( $post && ! $this->check_read_post_permission( $post, $request ) ) {
return new WP_Error(
'rest_cannot_read_post',
__( 'Sorry, you are not allowed to read the post for this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
*/
public function get_item( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
$data = $this->prepare_item_for_response( $comment, $request );
$response = rest_ensure_response( $data );
return $response;
}
/**
* Checks if a given request has access to create a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! is_user_logged_in() ) {
if ( get_option( 'comment_registration' ) ) {
return new WP_Error(
'rest_comment_login_required',
__( 'Sorry, you must be logged in to comment.' ),
array( 'status' => 401 )
);
}
/**
* Filters whether comments can be created via the REST API without authentication.
*
* Enables creating comments for anonymous users.
*
* @since 4.7.0
*
* @param bool $allow_anonymous Whether to allow anonymous comments to
* be created. Default `false`.
* @param WP_REST_Request $request Request used to generate the
* response.
*/
$allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );
if ( ! $allow_anonymous ) {
return new WP_Error(
'rest_comment_login_required',
__( 'Sorry, you must be logged in to comment.' ),
array( 'status' => 401 )
);
}
}
// Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_comment_invalid_author',
/* translators: %s: Request parameter. */
sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) {
if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) {
return new WP_Error(
'rest_comment_invalid_author_ip',
/* translators: %s: Request parameter. */
sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
return new WP_Error(
'rest_comment_invalid_status',
/* translators: %s: Request parameter. */
sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( empty( $request['post'] ) ) {
return new WP_Error(
'rest_comment_invalid_post_id',
__( 'Sorry, you are not allowed to create this comment without a post.' ),
array( 'status' => 403 )
);
}
$post = get_post( (int) $request['post'] );
if ( ! $post ) {
return new WP_Error(
'rest_comment_invalid_post_id',
__( 'Sorry, you are not allowed to create this comment without a post.' ),
array( 'status' => 403 )
);
}
if ( 'draft' === $post->post_status ) {
return new WP_Error(
'rest_comment_draft_post',
__( 'Sorry, you are not allowed to create a comment on this post.' ),
array( 'status' => 403 )
);
}
if ( 'trash' === $post->post_status ) {
return new WP_Error(
'rest_comment_trash_post',
__( 'Sorry, you are not allowed to create a comment on this post.' ),
array( 'status' => 403 )
);
}
if ( ! $this->check_read_post_permission( $post, $request ) ) {
return new WP_Error(
'rest_cannot_read_post',
__( 'Sorry, you are not allowed to read the post for this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! comments_open( $post->ID ) ) {
return new WP_Error(
'rest_comment_closed',
__( 'Sorry, comments are closed for this item.' ),
array( 'status' => 403 )
);
}
return true;
}
/**
* Creates a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_comment_exists',
__( 'Cannot create existing comment.' ),
array( 'status' => 400 )
);
}
// Do not allow comments to be created with a non-default type.
if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) {
return new WP_Error(
'rest_invalid_comment_type',
__( 'Cannot create a comment with that type.' ),
array( 'status' => 400 )
);
}
$prepared_comment = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_comment ) ) {
return $prepared_comment;
}
$prepared_comment['comment_type'] = 'comment';
if ( ! isset( $prepared_comment['comment_content'] ) ) {
$prepared_comment['comment_content'] = '';
}
if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) {
return new WP_Error(
'rest_comment_content_invalid',
__( 'Invalid comment content.' ),
array( 'status' => 400 )
);
}
// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
$prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
}
// Set author data if the user's logged in.
$missing_author = empty( $prepared_comment['user_id'] )
&& empty( $prepared_comment['comment_author'] )
&& empty( $prepared_comment['comment_author_email'] )
&& empty( $prepared_comment['comment_author_url'] );
if ( is_user_logged_in() && $missing_author ) {
$user = wp_get_current_user();
$prepared_comment['user_id'] = $user->ID;
$prepared_comment['comment_author'] = $user->display_name;
$prepared_comment['comment_author_email'] = $user->user_email;
$prepared_comment['comment_author_url'] = $user->user_url;
}
// Honor the discussion setting that requires a name and email address of the comment author.
if ( get_option( 'require_name_email' ) ) {
if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) {
return new WP_Error(
'rest_comment_author_data_required',
__( 'Creating a comment requires valid author name and email values.' ),
array( 'status' => 400 )
);
}
}
if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
$prepared_comment['comment_author_email'] = '';
}
if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
$prepared_comment['comment_author_url'] = '';
}
if ( ! isset( $prepared_comment['comment_agent'] ) ) {
$prepared_comment['comment_agent'] = '';
}
$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment );
if ( is_wp_error( $check_comment_lengths ) ) {
$error_code = $check_comment_lengths->get_error_code();
return new WP_Error(
$error_code,
__( 'Comment field exceeds maximum length allowed.' ),
array( 'status' => 400 )
);
}
$prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );
if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
$error_code = $prepared_comment['comment_approved']->get_error_code();
$error_message = $prepared_comment['comment_approved']->get_error_message();
if ( 'comment_duplicate' === $error_code ) {
return new WP_Error(
$error_code,
$error_message,
array( 'status' => 409 )
);
}
if ( 'comment_flood' === $error_code ) {
return new WP_Error(
$error_code,
$error_message,
array( 'status' => 400 )
);
}
return $prepared_comment['comment_approved'];
}
/**
* Filters a comment before it is inserted via the REST API.
*
* Allows modification of the comment right before it is inserted via wp_insert_comment().
* Returning a WP_Error value from the filter will short-circuit insertion and allow
* skipping further processing.
*
* @since 4.7.0
* @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion.
*
* @param array|WP_Error $prepared_comment The prepared comment data for wp_insert_comment().
* @param WP_REST_Request $request Request used to insert the comment.
*/
$prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
if ( is_wp_error( $prepared_comment ) ) {
return $prepared_comment;
}
$comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) );
if ( ! $comment_id ) {
return new WP_Error(
'rest_comment_failed_create',
__( 'Creating comment failed.' ),
array( 'status' => 500 )
);
}
if ( isset( $request['status'] ) ) {
$this->handle_status_param( $request['status'], $comment_id );
}
$comment = get_comment( $comment_id );
/**
* Fires after a comment is created or updated via the REST API.
*
* @since 4.7.0
*
* @param WP_Comment $comment Inserted or updated comment object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a comment, false
* when updating.
*/
do_action( 'rest_insert_comment', $comment, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $comment_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $comment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
$request->set_param( 'context', $context );
/**
* Fires completely after a comment is created or updated via the REST API.
*
* @since 5.0.0
*
* @param WP_Comment $comment Inserted or updated comment object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a comment, false
* when updating.
*/
do_action( 'rest_after_insert_comment', $comment, $request, true );
$response = $this->prepare_item_for_response( $comment, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );
return $response;
}
/**
* Checks if a given REST request has access to update a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
if ( ! $this->check_edit_permission( $comment ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
*/
public function update_item( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
$id = $comment->comment_ID;
if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
return new WP_Error(
'rest_comment_invalid_type',
__( 'Sorry, you are not allowed to change the comment type.' ),
array( 'status' => 404 )
);
}
$prepared_args = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_args ) ) {
return $prepared_args;
}
if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
$post = get_post( $prepared_args['comment_post_ID'] );
if ( empty( $post ) ) {
return new WP_Error(
'rest_comment_invalid_post_id',
__( 'Invalid post ID.' ),
array( 'status' => 403 )
);
}
}
if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
// Only the comment status is being changed.
$change = $this->handle_status_param( $request['status'], $id );
if ( ! $change ) {
return new WP_Error(
'rest_comment_failed_edit',
__( 'Updating comment status failed.' ),
array( 'status' => 500 )
);
}
} elseif ( ! empty( $prepared_args ) ) {
if ( is_wp_error( $prepared_args ) ) {
return $prepared_args;
}
if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
return new WP_Error(
'rest_comment_content_invalid',
__( 'Invalid comment content.' ),
array( 'status' => 400 )
);
}
$prepared_args['comment_ID'] = $id;
$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
if ( is_wp_error( $check_comment_lengths ) ) {
$error_code = $check_comment_lengths->get_error_code();
return new WP_Error(
$error_code,
__( 'Comment field exceeds maximum length allowed.' ),
array( 'status' => 400 )
);
}
$updated = wp_update_comment( wp_slash( (array) $prepared_args ), true );
if ( is_wp_error( $updated ) ) {
return new WP_Error(
'rest_comment_failed_edit',
__( 'Updating comment failed.' ),
array( 'status' => 500 )
);
}
if ( isset( $request['status'] ) ) {
$this->handle_status_param( $request['status'], $id );
}
}
$comment = get_comment( $id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
do_action( 'rest_insert_comment', $comment, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $comment, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
do_action( 'rest_after_insert_comment', $comment, $request, false );
$response = $this->prepare_item_for_response( $comment, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a given request has access to delete a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
if ( ! $this->check_edit_permission( $comment ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this comment.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a comment.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
*/
public function delete_item( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
/**
* Filters whether a comment can be trashed via the REST API.
*
* Return false to disable trash support for the comment.
*
* @since 4.7.0
*
* @param bool $supports_trash Whether the comment supports trashing.
* @param WP_Comment $comment The comment object being considered for trashing support.
*/
$supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );
$request->set_param( 'context', 'edit' );
if ( $force ) {
$previous = $this->prepare_item_for_response( $comment, $request );
$result = wp_delete_comment( $comment->comment_ID, true );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
} else {
// If this type doesn't support trashing, error out.
if ( ! $supports_trash ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "The comment does not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
if ( 'trash' === $comment->comment_approved ) {
return new WP_Error(
'rest_already_trashed',
__( 'The comment has already been trashed.' ),
array( 'status' => 410 )
);
}
$result = wp_trash_comment( $comment->comment_ID );
$comment = get_comment( $comment->comment_ID );
$response = $this->prepare_item_for_response( $comment, $request );
}
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The comment cannot be deleted.' ),
array( 'status' => 500 )
);
}
/**
* Fires after a comment is deleted via the REST API.
*
* @since 4.7.0
*
* @param WP_Comment $comment The deleted comment data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_comment', $comment, $response, $request );
return $response;
}
/**
* Prepares a single comment output for response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Comment $item Comment object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$comment = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'id', $fields, true ) ) {
$data['id'] = (int) $comment->comment_ID;
}
if ( in_array( 'post', $fields, true ) ) {
$data['post'] = (int) $comment->comment_post_ID;
}
if ( in_array( 'parent', $fields, true ) ) {
$data['parent'] = (int) $comment->comment_parent;
}
if ( in_array( 'author', $fields, true ) ) {
$data['author'] = (int) $comment->user_id;
}
if ( in_array( 'author_name', $fields, true ) ) {
$data['author_name'] = $comment->comment_author;
}
if ( in_array( 'author_email', $fields, true ) ) {
$data['author_email'] = $comment->comment_author_email;
}
if ( in_array( 'author_url', $fields, true ) ) {
$data['author_url'] = $comment->comment_author_url;
}
if ( in_array( 'author_ip', $fields, true ) ) {
$data['author_ip'] = $comment->comment_author_IP;
}
if ( in_array( 'author_user_agent', $fields, true ) ) {
$data['author_user_agent'] = $comment->comment_agent;
}
if ( in_array( 'date', $fields, true ) ) {
$data['date'] = mysql_to_rfc3339( $comment->comment_date );
}
if ( in_array( 'date_gmt', $fields, true ) ) {
$data['date_gmt'] = mysql_to_rfc3339( $comment->comment_date_gmt );
}
if ( in_array( 'content', $fields, true ) ) {
$data['content'] = array(
/** This filter is documented in wp-includes/comment-template.php */
'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ),
'raw' => $comment->comment_content,
);
}
if ( in_array( 'link', $fields, true ) ) {
$data['link'] = get_comment_link( $comment );
}
if ( in_array( 'status', $fields, true ) ) {
$data['status'] = $this->prepare_status_response( $comment->comment_approved );
}
if ( in_array( 'type', $fields, true ) ) {
$data['type'] = get_comment_type( $comment->comment_ID );
}
if ( in_array( 'author_avatar_urls', $fields, true ) ) {
$data['author_avatar_urls'] = rest_get_avatar_urls( $comment );
}
if ( in_array( 'meta', $fields, true ) ) {
$data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $comment ) );
}
/**
* Filters a comment returned from the REST API.
*
* Allows modification of the comment right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $comment The original comment object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
}
/**
* Prepares links for the request.
*
* @since 4.7.0
*
* @param WP_Comment $comment Comment object.
* @return array Links for the given comment.
*/
protected function prepare_links( $comment ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( 0 !== (int) $comment->user_id ) {
$links['author'] = array(
'href' => rest_url( 'wp/v2/users/' . $comment->user_id ),
'embeddable' => true,
);
}
if ( 0 !== (int) $comment->comment_post_ID ) {
$post = get_post( $comment->comment_post_ID );
$post_route = rest_get_route_for_post( $post );
if ( ! empty( $post->ID ) && $post_route ) {
$links['up'] = array(
'href' => rest_url( $post_route ),
'embeddable' => true,
'post_type' => $post->post_type,
);
}
}
if ( 0 !== (int) $comment->comment_parent ) {
$links['in-reply-to'] = array(
'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
'embeddable' => true,
);
}
// Only grab one comment to verify the comment has children.
$comment_children = $comment->get_children(
array(
'count' => true,
'orderby' => 'none',
)
);
if ( ! empty( $comment_children ) ) {
$args = array(
'parent' => $comment->comment_ID,
);
$rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );
$links['children'] = array(
'href' => $rest_url,
'embeddable' => true,
);
}
return $links;
}
/**
* Prepends internal property prefix to query parameters to match our response fields.
*
* @since 4.7.0
*
* @param string $query_param Query parameter.
* @return string The normalized query parameter.
*/
protected function normalize_query_param( $query_param ) {
$prefix = 'comment_';
switch ( $query_param ) {
case 'id':
$normalized = $prefix . 'ID';
break;
case 'post':
$normalized = $prefix . 'post_ID';
break;
case 'parent':
$normalized = $prefix . 'parent';
break;
case 'include':
$normalized = 'comment__in';
break;
default:
$normalized = $prefix . $query_param;
break;
}
return $normalized;
}
/**
* Checks comment_approved to set comment status for single comment output.
*
* @since 4.7.0
*
* @param string|int $comment_approved comment status.
* @return string Comment status.
*/
protected function prepare_status_response( $comment_approved ) {
switch ( $comment_approved ) {
case 'hold':
case '0':
$status = 'hold';
break;
case 'approve':
case '1':
$status = 'approved';
break;
case 'spam':
case 'trash':
default:
$status = $comment_approved;
break;
}
return $status;
}
/**
* Prepares a single comment to be inserted into the database.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return array|WP_Error Prepared comment, otherwise WP_Error object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_comment = array();
/*
* Allow the comment_content to be set via the 'content' or
* the 'content.raw' properties of the Request object.
*/
if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
$prepared_comment['comment_content'] = trim( $request['content'] );
} elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
$prepared_comment['comment_content'] = trim( $request['content']['raw'] );
}
if ( isset( $request['post'] ) ) {
$prepared_comment['comment_post_ID'] = (int) $request['post'];
}
if ( isset( $request['parent'] ) ) {
$prepared_comment['comment_parent'] = $request['parent'];
}
if ( isset( $request['author'] ) ) {
$user = new WP_User( $request['author'] );
if ( $user->exists() ) {
$prepared_comment['user_id'] = $user->ID;
$prepared_comment['comment_author'] = $user->display_name;
$prepared_comment['comment_author_email'] = $user->user_email;
$prepared_comment['comment_author_url'] = $user->user_url;
} else {
return new WP_Error(
'rest_comment_author_invalid',
__( 'Invalid comment author ID.' ),
array( 'status' => 400 )
);
}
}
if ( isset( $request['author_name'] ) ) {
$prepared_comment['comment_author'] = $request['author_name'];
}
if ( isset( $request['author_email'] ) ) {
$prepared_comment['comment_author_email'] = $request['author_email'];
}
if ( isset( $request['author_url'] ) ) {
$prepared_comment['comment_author_url'] = $request['author_url'];
}
if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) {
$prepared_comment['comment_author_IP'] = $request['author_ip'];
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) {
$prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR'];
} else {
$prepared_comment['comment_author_IP'] = '127.0.0.1';
}
if ( ! empty( $request['author_user_agent'] ) ) {
$prepared_comment['comment_agent'] = $request['author_user_agent'];
} elseif ( $request->get_header( 'user_agent' ) ) {
$prepared_comment['comment_agent'] = $request->get_header( 'user_agent' );
}
if ( ! empty( $request['date'] ) ) {
$date_data = rest_get_date_with_gmt( $request['date'] );
if ( ! empty( $date_data ) ) {
list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
}
} elseif ( ! empty( $request['date_gmt'] ) ) {
$date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
if ( ! empty( $date_data ) ) {
list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
}
}
/**
* Filters a comment added via the REST API after it is prepared for insertion into the database.
*
* Allows modification of the comment right after it is prepared for the database.
*
* @since 4.7.0
*
* @param array $prepared_comment The prepared comment data for `wp_insert_comment`.
* @param WP_REST_Request $request The current request.
*/
return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
}
/**
* Retrieves the comment's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'comment',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the comment.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'author' => array(
'description' => __( 'The ID of the user object, if author was a user.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'author_email' => array(
'description' => __( 'Email address for the comment author.' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'check_comment_author_email' ),
'validate_callback' => null, // Skip built-in validation of 'email'.
),
),
'author_ip' => array(
'description' => __( 'IP address for the comment author.' ),
'type' => 'string',
'format' => 'ip',
'context' => array( 'edit' ),
),
'author_name' => array(
'description' => __( 'Display name for the comment author.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'author_url' => array(
'description' => __( 'URL for the comment author.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
),
'author_user_agent' => array(
'description' => __( 'User agent for the comment author.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'content' => array(
'description' => __( 'The content for the comment.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Content for the comment, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML content for the comment, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
),
'date' => array(
'description' => __( "The date the comment was published, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the comment was published, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'link' => array(
'description' => __( 'URL to the comment.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'parent' => array(
'description' => __( 'The ID for the parent of the comment.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'default' => 0,
),
'post' => array(
'description' => __( 'The ID of the associated post object.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'default' => 0,
),
'status' => array(
'description' => __( 'State of the comment.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_key',
),
),
'type' => array(
'description' => __( 'Type of the comment.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
if ( get_option( 'show_avatars' ) ) {
$avatar_properties = array();
$avatar_sizes = rest_get_avatar_sizes();
foreach ( $avatar_sizes as $size ) {
$avatar_properties[ $size ] = array(
/* translators: %d: Avatar image size in pixels. */
'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$schema['properties']['author_avatar_urls'] = array(
'description' => __( 'Avatar URLs for the comment author.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
'properties' => $avatar_properties,
);
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Comments collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['after'] = array(
'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['author'] = array(
'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$query_params['author_exclude'] = array(
'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$query_params['author_email'] = array(
'default' => null,
'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
'format' => 'email',
'type' => 'string',
);
$query_params['before'] = array(
'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'desc',
'enum' => array(
'asc',
'desc',
),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by comment attribute.' ),
'type' => 'string',
'default' => 'date_gmt',
'enum' => array(
'date',
'date_gmt',
'id',
'include',
'post',
'parent',
'type',
),
);
$query_params['parent'] = array(
'default' => array(),
'description' => __( 'Limit result set to comments of specific parent IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$query_params['parent_exclude'] = array(
'default' => array(),
'description' => __( 'Ensure result set excludes specific parent IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$query_params['post'] = array(
'default' => array(),
'description' => __( 'Limit result set to comments assigned to specific post IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$query_params['status'] = array(
'default' => 'approve',
'description' => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['type'] = array(
'default' => 'comment',
'description' => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$query_params['password'] = array(
'description' => __( 'The password for the post if it is password protected.' ),
'type' => 'string',
);
/**
* Filters REST API collection parameters for the comments controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Comment_Query parameter. Use the
* `rest_comment_query` filter to set WP_Comment_Query parameters.
*
* @since 4.7.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_comment_collection_params', $query_params );
}
/**
* Sets the comment_status of a given comment object when creating or updating a comment.
*
* @since 4.7.0
*
* @param string|int $new_status New comment status.
* @param int $comment_id Comment ID.
* @return bool Whether the status was changed.
*/
protected function handle_status_param( $new_status, $comment_id ) {
$old_status = wp_get_comment_status( $comment_id );
if ( $new_status === $old_status ) {
return false;
}
switch ( $new_status ) {
case 'approved':
case 'approve':
case '1':
$changed = wp_set_comment_status( $comment_id, 'approve' );
break;
case 'hold':
case '0':
$changed = wp_set_comment_status( $comment_id, 'hold' );
break;
case 'spam':
$changed = wp_spam_comment( $comment_id );
break;
case 'unspam':
$changed = wp_unspam_comment( $comment_id );
break;
case 'trash':
$changed = wp_trash_comment( $comment_id );
break;
case 'untrash':
$changed = wp_untrash_comment( $comment_id );
break;
default:
$changed = false;
break;
}
return $changed;
}
/**
* Checks if the post can be read.
*
* Correctly handles posts with the inherit status.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request data to check.
* @return bool Whether post can be read.
*/
protected function check_read_post_permission( $post, $request ) {
$post_type = get_post_type_object( $post->post_type );
// Return false if custom post type doesn't exist
if ( ! $post_type ) {
return false;
}
$posts_controller = $post_type->get_rest_controller();
/*
* Ensure the posts controller is specifically a WP_REST_Posts_Controller instance
* before using methods specific to that controller.
*/
if ( ! $posts_controller instanceof WP_REST_Posts_Controller ) {
$posts_controller = new WP_REST_Posts_Controller( $post->post_type );
}
$has_password_filter = false;
// Only check password if a specific post was queried for or a single comment
$requested_post = ! empty( $request['post'] ) && ( ! is_array( $request['post'] ) || 1 === count( $request['post'] ) );
$requested_comment = ! empty( $request['id'] );
if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) {
add_filter( 'post_password_required', '__return_false' );
$has_password_filter = true;
}
if ( post_password_required( $post ) ) {
$result = current_user_can( 'edit_post', $post->ID );
} else {
$result = $posts_controller->check_read_permission( $post );
}
if ( $has_password_filter ) {
remove_filter( 'post_password_required', '__return_false' );
}
return $result;
}
/**
* Checks if the comment can be read.
*
* @since 4.7.0
*
* @param WP_Comment $comment Comment object.
* @param WP_REST_Request $request Request data to check.
* @return bool Whether the comment can be read.
*/
protected function check_read_permission( $comment, $request ) {
if ( ! empty( $comment->comment_post_ID ) ) {
$post = get_post( $comment->comment_post_ID );
if ( $post ) {
if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
return true;
}
}
}
if ( 0 === get_current_user_id() ) {
return false;
}
if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
return false;
}
if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
return true;
}
return current_user_can( 'edit_comment', $comment->comment_ID );
}
/**
* Checks if a comment can be edited or deleted.
*
* @since 4.7.0
*
* @param WP_Comment $comment Comment object.
* @return bool Whether the comment can be edited or deleted.
*/
protected function check_edit_permission( $comment ) {
if ( 0 === (int) get_current_user_id() ) {
return false;
}
if ( current_user_can( 'moderate_comments' ) ) {
return true;
}
return current_user_can( 'edit_comment', $comment->comment_ID );
}
/**
* Checks a comment author email for validity.
*
* Accepts either a valid email address or empty string as a valid comment
* author email address. Setting the comment author email to an empty
* string is allowed when a comment is being updated.
*
* @since 4.7.0
*
* @param string $value Author email value submitted.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter name.
* @return string|WP_Error The sanitized email address, if valid,
* otherwise an error.
*/
public function check_comment_author_email( $value, $request, $param ) {
$email = (string) $value;
if ( empty( $email ) ) {
return $email;
}
$check_email = rest_validate_request_arg( $email, $request, $param );
if ( is_wp_error( $check_email ) ) {
return $check_email;
}
return $email;
}
/**
* If empty comments are not allowed, checks if the provided comment content is not empty.
*
* @since 5.6.0
*
* @param array $prepared_comment The prepared comment data.
* @return bool True if the content is allowed, false otherwise.
*/
protected function check_is_comment_content_allowed( $prepared_comment ) {
$check = wp_parse_args(
$prepared_comment,
array(
'comment_post_ID' => 0,
'comment_author' => null,
'comment_author_email' => null,
'comment_author_url' => null,
'comment_parent' => 0,
'user_id' => 0,
)
);
/** This filter is documented in wp-includes/comment.php */
$allow_empty = apply_filters( 'allow_empty_comment', false, $check );
if ( $allow_empty ) {
return true;
}
/*
* Do not allow a comment to be created with missing or empty
* comment_content. See wp_handle_comment_submission().
*/
return '' !== $check['comment_content'];
}
}
endpoints/class-wp-rest-controller.php 0000644 00000045174 15153067731 0014170 0 ustar 00 <?php
/**
* REST API: WP_REST_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core base controller for managing and interacting with REST API items.
*
* @since 4.7.0
*/
#[AllowDynamicProperties]
abstract class WP_REST_Controller {
/**
* The namespace of this controller's route.
*
* @since 4.7.0
* @var string
*/
protected $namespace;
/**
* The base of this controller's route.
*
* @since 4.7.0
* @var string
*/
protected $rest_base;
/**
* Cached results of get_item_schema.
*
* @since 5.3.0
* @var array
*/
protected $schema;
/**
* Registers the routes for the objects of the controller.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
_doing_it_wrong(
'WP_REST_Controller::register_routes',
/* translators: %s: register_routes() */
sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ),
'4.7.0'
);
}
/**
* Checks if a given request has access to get items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Retrieves a collection of items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Checks if a given request has access to get a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Retrieves one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Checks if a given request has access to create items.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Creates one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Checks if a given request has access to update a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Updates one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Checks if a given request has access to delete a specific item.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Deletes one item from the collection.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Prepares one item for create or update operation.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return object|WP_Error The prepared item, or WP_Error object on failure.
*/
protected function prepare_item_for_database( $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Prepares the item for the REST response.
*
* @since 4.7.0
*
* @param mixed $item WordPress representation of the item.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
return new WP_Error(
'invalid-method',
/* translators: %s: Method name. */
sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
array( 'status' => 405 )
);
}
/**
* Prepares a response for insertion into a collection.
*
* @since 4.7.0
*
* @param WP_REST_Response $response Response object.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function prepare_response_for_collection( $response ) {
if ( ! ( $response instanceof WP_REST_Response ) ) {
return $response;
}
$data = (array) $response->get_data();
$server = rest_get_server();
$links = $server::get_compact_response_links( $response );
if ( ! empty( $links ) ) {
$data['_links'] = $links;
}
return $data;
}
/**
* Filters a response based on the context defined in the schema.
*
* @since 4.7.0
*
* @param array $response_data Response data to filter.
* @param string $context Context defined in the schema.
* @return array Filtered response.
*/
public function filter_response_by_context( $response_data, $context ) {
$schema = $this->get_item_schema();
return rest_filter_response_by_context( $response_data, $schema, $context );
}
/**
* Retrieves the item's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
return $this->add_additional_fields_schema( array() );
}
/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @since 4.7.0
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties'] ) ) {
foreach ( $schema['properties'] as &$property ) {
unset( $property['arg_options'] );
}
}
return $schema;
}
/**
* Retrieves the query params for the collections.
*
* @since 4.7.0
*
* @return array Query parameters for the collection.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param(),
'page' => array(
'description' => __( 'Current page of the collection.' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => __( 'Maximum number of items to be returned in result set.' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => __( 'Limit results to those matching a string.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
/**
* Retrieves the magical context param.
*
* Ensures consistent descriptions between endpoints, and populates enum from schema.
*
* @since 4.7.0
*
* @param array $args Optional. Additional arguments for context parameter. Default empty array.
* @return array Context parameter details.
*/
public function get_context_param( $args = array() ) {
$param_details = array(
'description' => __( 'Scope under which the request is made; determines fields present in response.' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$schema = $this->get_item_schema();
if ( empty( $schema['properties'] ) ) {
return array_merge( $param_details, $args );
}
$contexts = array();
foreach ( $schema['properties'] as $attributes ) {
if ( ! empty( $attributes['context'] ) ) {
$contexts = array_merge( $contexts, $attributes['context'] );
}
}
if ( ! empty( $contexts ) ) {
$param_details['enum'] = array_unique( $contexts );
rsort( $param_details['enum'] );
}
return array_merge( $param_details, $args );
}
/**
* Adds the values from additional fields to a data object.
*
* @since 4.7.0
*
* @param array $response_data Prepared response array.
* @param WP_REST_Request $request Full details about the request.
* @return array Modified data object with additional fields.
*/
protected function add_additional_fields_to_object( $response_data, $request ) {
$additional_fields = $this->get_additional_fields();
$requested_fields = $this->get_fields_for_response( $request );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['get_callback'] ) {
continue;
}
if ( ! rest_is_field_included( $field_name, $requested_fields ) ) {
continue;
}
$response_data[ $field_name ] = call_user_func(
$field_options['get_callback'],
$response_data,
$field_name,
$request,
$this->get_object_type()
);
}
return $response_data;
}
/**
* Updates the values of additional fields added to a data object.
*
* @since 4.7.0
*
* @param object $data_object Data model like WP_Term or WP_Post.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True on success, WP_Error object if a field cannot be updated.
*/
protected function update_additional_fields_for_object( $data_object, $request ) {
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['update_callback'] ) {
continue;
}
// Don't run the update callbacks if the data wasn't passed in the request.
if ( ! isset( $request[ $field_name ] ) ) {
continue;
}
$result = call_user_func(
$field_options['update_callback'],
$request[ $field_name ],
$data_object,
$field_name,
$request,
$this->get_object_type()
);
if ( is_wp_error( $result ) ) {
return $result;
}
}
return true;
}
/**
* Adds the schema from additional fields to a schema array.
*
* The type of object is inferred from the passed schema.
*
* @since 4.7.0
*
* @param array $schema Schema array.
* @return array Modified Schema array.
*/
protected function add_additional_fields_schema( $schema ) {
if ( empty( $schema['title'] ) ) {
return $schema;
}
// Can't use $this->get_object_type otherwise we cause an inf loop.
$object_type = $schema['title'];
$additional_fields = $this->get_additional_fields( $object_type );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['schema'] ) {
continue;
}
$schema['properties'][ $field_name ] = $field_options['schema'];
}
return $schema;
}
/**
* Retrieves all of the registered additional fields for a given object-type.
*
* @since 4.7.0
*
* @global array $wp_rest_additional_fields Holds registered fields, organized by object type.
*
* @param string $object_type Optional. The object type.
* @return array Registered additional fields (if any), empty array if none or if the object type
* could not be inferred.
*/
protected function get_additional_fields( $object_type = null ) {
global $wp_rest_additional_fields;
if ( ! $object_type ) {
$object_type = $this->get_object_type();
}
if ( ! $object_type ) {
return array();
}
if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
return array();
}
return $wp_rest_additional_fields[ $object_type ];
}
/**
* Retrieves the object type this controller is responsible for managing.
*
* @since 4.7.0
*
* @return string Object type for the controller.
*/
protected function get_object_type() {
$schema = $this->get_item_schema();
if ( ! $schema || ! isset( $schema['title'] ) ) {
return null;
}
return $schema['title'];
}
/**
* Gets an array of fields to be included on the response.
*
* Included fields are based on item schema and `_fields=` request argument.
*
* @since 4.9.6
*
* @param WP_REST_Request $request Full details about the request.
* @return string[] Fields to be included in the response.
*/
public function get_fields_for_response( $request ) {
$schema = $this->get_item_schema();
$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
/*
* For back-compat, include any field with an empty schema
* because it won't be present in $this->get_item_schema().
*/
if ( is_null( $field_options['schema'] ) ) {
$properties[ $field_name ] = $field_options;
}
}
// Exclude fields that specify a different context than the request context.
$context = $request['context'];
if ( $context ) {
foreach ( $properties as $name => $options ) {
if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
unset( $properties[ $name ] );
}
}
}
$fields = array_keys( $properties );
/*
* '_links' and '_embedded' are not typically part of the item schema,
* but they can be specified in '_fields', so they are added here as a
* convenience for checking with rest_is_field_included().
*/
$fields[] = '_links';
if ( $request->has_param( '_embed' ) ) {
$fields[] = '_embedded';
}
$fields = array_unique( $fields );
if ( ! isset( $request['_fields'] ) ) {
return $fields;
}
$requested_fields = wp_parse_list( $request['_fields'] );
if ( 0 === count( $requested_fields ) ) {
return $fields;
}
// Trim off outside whitespace from the comma delimited list.
$requested_fields = array_map( 'trim', $requested_fields );
// Always persist 'id', because it can be needed for add_additional_fields_to_object().
if ( in_array( 'id', $fields, true ) ) {
$requested_fields[] = 'id';
}
// Return the list of all requested fields which appear in the schema.
return array_reduce(
$requested_fields,
static function ( $response_fields, $field ) use ( $fields ) {
if ( in_array( $field, $fields, true ) ) {
$response_fields[] = $field;
return $response_fields;
}
// Check for nested fields if $field is not a direct match.
$nested_fields = explode( '.', $field );
/*
* A nested field is included so long as its top-level property
* is present in the schema.
*/
if ( in_array( $nested_fields[0], $fields, true ) ) {
$response_fields[] = $field;
}
return $response_fields;
},
array()
);
}
/**
* Retrieves an array of endpoint arguments from the item schema for the controller.
*
* @since 4.7.0
*
* @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
* checked for required values and may fall-back to a given default, this is not done
* on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
return rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );
}
/**
* Sanitizes the slug value.
*
* @since 4.7.0
*
* @internal We can't use sanitize_title() directly, as the second
* parameter is the fallback title, which would end up being set to the
* request object.
*
* @see https://github.com/WP-API/WP-API/issues/1585
*
* @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
*
* @param string $slug Slug value passed in request.
* @return string Sanitized value for the slug.
*/
public function sanitize_slug( $slug ) {
return sanitize_title( $slug );
}
}
endpoints/class-wp-rest-edit-site-export-controller.php 0000644 00000004112 15153067731 0017357 0 ustar 00 <?php
/**
* REST API: WP_REST_Edit_Site_Export_Controller class
*
* @package WordPress
* @subpackage REST_API
*/
/**
* Controller which provides REST endpoint for exporting current templates
* and template parts.
*
* @since 5.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Edit_Site_Export_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 5.9.0
*/
public function __construct() {
$this->namespace = 'wp-block-editor/v1';
$this->rest_base = 'export';
}
/**
* Registers the site export route.
*
* @since 5.9.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'export' ),
'permission_callback' => array( $this, 'permissions_check' ),
),
)
);
}
/**
* Checks whether a given request has permission to export.
*
* @since 5.9.0
*
* @return WP_Error|true True if the request has access, or WP_Error object.
*/
public function permissions_check() {
if ( current_user_can( 'edit_theme_options' ) ) {
return true;
}
return new WP_Error(
'rest_cannot_export_templates',
__( 'Sorry, you are not allowed to export templates and template parts.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Output a ZIP file with an export of the current templates
* and template parts from the site editor, and close the connection.
*
* @since 5.9.0
*
* @return WP_Error|void
*/
public function export() {
// Generate the export file.
$filename = wp_generate_block_templates_export_file();
if ( is_wp_error( $filename ) ) {
$filename->add_data( array( 'status' => 500 ) );
return $filename;
}
$theme_name = basename( get_stylesheet() );
header( 'Content-Type: application/zip' );
header( 'Content-Disposition: attachment; filename=' . $theme_name . '.zip' );
header( 'Content-Length: ' . filesize( $filename ) );
flush();
readfile( $filename );
unlink( $filename );
exit;
}
}
endpoints/class-wp-rest-font-collections-controller.php 0000644 00000022272 15153067731 0017442 0 ustar 00 <?php
/**
* Rest Font Collections Controller.
*
* This file contains the class for the REST API Font Collections Controller.
*
* @package WordPress
* @subpackage REST_API
* @since 6.5.0
*/
/**
* Font Library Controller class.
*
* @since 6.5.0
*/
class WP_REST_Font_Collections_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 6.5.0
*/
public function __construct() {
$this->rest_base = 'font-collections';
$this->namespace = 'wp/v2';
}
/**
* Registers the routes for the objects of the controller.
*
* @since 6.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<slug>[\/\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Gets the font collections available.
*
* @since 6.5.0
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$collections_all = WP_Font_Library::get_instance()->get_font_collections();
$page = $request['page'];
$per_page = $request['per_page'];
$total_items = count( $collections_all );
$max_pages = (int) ceil( $total_items / $per_page );
if ( $page > $max_pages && $total_items > 0 ) {
return new WP_Error(
'rest_post_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
$collections_page = array_slice( $collections_all, ( $page - 1 ) * $per_page, $per_page );
$items = array();
foreach ( $collections_page as $collection ) {
$item = $this->prepare_item_for_response( $collection, $request );
// If there's an error loading a collection, skip it and continue loading valid collections.
if ( is_wp_error( $item ) ) {
continue;
}
$item = $this->prepare_response_for_collection( $item );
$items[] = $item;
}
$response = rest_ensure_response( $items );
$response->header( 'X-WP-Total', (int) $total_items );
$response->header( 'X-WP-TotalPages', $max_pages );
$request_params = $request->get_query_params();
$collection_url = rest_url( $this->namespace . '/' . $this->rest_base );
$base = add_query_arg( urlencode_deep( $request_params ), $collection_url );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Gets a font collection.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$slug = $request->get_param( 'slug' );
$collection = WP_Font_Library::get_instance()->get_font_collection( $slug );
if ( ! $collection ) {
return new WP_Error( 'rest_font_collection_not_found', __( 'Font collection not found.' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $collection, $request );
}
/**
* Prepare a single collection output for response.
*
* @since 6.5.0
*
* @param WP_Font_Collection $item Font collection object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'slug', $fields ) ) {
$data['slug'] = $item->slug;
}
// If any data fields are requested, get the collection data.
$data_fields = array( 'name', 'description', 'font_families', 'categories' );
if ( ! empty( array_intersect( $fields, $data_fields ) ) ) {
$collection_data = $item->get_data();
if ( is_wp_error( $collection_data ) ) {
$collection_data->add_data( array( 'status' => 500 ) );
return $collection_data;
}
foreach ( $data_fields as $field ) {
if ( rest_is_field_included( $field, $fields ) ) {
$data[ $field ] = $collection_data[ $field ];
}
}
}
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) ) {
$links = $this->prepare_links( $item );
$response->add_links( $links );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$response->data = $this->add_additional_fields_to_object( $response->data, $request );
$response->data = $this->filter_response_by_context( $response->data, $context );
/**
* Filters the font collection data for a REST API response.
*
* @since 6.5.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Font_Collection $item The font collection object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_font_collection', $response, $item, $request );
}
/**
* Retrieves the font collection's schema, conforming to JSON Schema.
*
* @since 6.5.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'font-collection',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Unique identifier for the font collection.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The name for the font collection.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => __( 'The description for the font collection.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'font_families' => array(
'description' => __( 'The font families for the font collection.' ),
'type' => 'array',
'context' => array( 'view', 'edit', 'embed' ),
),
'categories' => array(
'description' => __( 'The categories for the font collection.' ),
'type' => 'array',
'context' => array( 'view', 'edit', 'embed' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Prepares links for the request.
*
* @since 6.5.0
*
* @param WP_Font_Collection $collection Font collection data
* @return array Links for the given font collection.
*/
protected function prepare_links( $collection ) {
return array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
}
/**
* Retrieves the search params for the font collections.
*
* @since 6.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
unset( $query_params['search'] );
/**
* Filters REST API collection parameters for the font collections controller.
*
* @since 6.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_font_collections_collection_params', $query_params );
}
/**
* Checks whether the user has permissions to use the Fonts Collections.
*
* @since 6.5.0
*
* @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( current_user_can( 'edit_theme_options' ) ) {
return true;
}
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to access font collections.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
}
endpoints/class-wp-rest-font-faces-controller.php 0000644 00000072164 15153067731 0016212 0 ustar 00 <?php
/**
* REST API: WP_REST_Font_Faces_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.5.0
*/
/**
* Class to access font faces through the REST API.
*/
class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller {
/**
* The latest version of theme.json schema supported by the controller.
*
* @since 6.5.0
* @var int
*/
const LATEST_THEME_JSON_VERSION_SUPPORTED = 2;
/**
* Whether the controller supports batching.
*
* @since 6.5.0
* @var false
*/
protected $allow_batch = false;
/**
* Registers the routes for posts.
*
* @since 6.5.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'args' => array(
'font_family_id' => array(
'description' => __( 'The ID for the parent font family of the font face.' ),
'type' => 'integer',
'required' => true,
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_create_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'font_family_id' => array(
'description' => __( 'The ID for the parent font family of the font face.' ),
'type' => 'integer',
'required' => true,
),
'id' => array(
'description' => __( 'Unique identifier for the font face.' ),
'type' => 'integer',
'required' => true,
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to font faces.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
if ( ! current_user_can( $post_type->cap->read ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to access font faces.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to a font face.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( ! current_user_can( 'read_post', $post->ID ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to access this font face.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Validates settings when creating a font face.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font face settings.
* @param WP_REST_Request $request Request object.
* @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
*/
public function validate_create_font_face_settings( $value, $request ) {
$settings = json_decode( $value, true );
// Check settings string is valid JSON.
if ( null === $settings ) {
return new WP_Error(
'rest_invalid_param',
__( 'font_face_settings parameter must be a valid JSON string.' ),
array( 'status' => 400 )
);
}
// Check that the font face settings match the theme.json schema.
$schema = $this->get_item_schema()['properties']['font_face_settings'];
$has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );
if ( is_wp_error( $has_valid_settings ) ) {
$has_valid_settings->add_data( array( 'status' => 400 ) );
return $has_valid_settings;
}
// Check that none of the required settings are empty values.
$required = $schema['required'];
foreach ( $required as $key ) {
if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: Name of the missing font face settings parameter, e.g. "font_face_settings[src]". */
sprintf( __( '%s cannot be empty.' ), "font_face_setting[ $key ]" ),
array( 'status' => 400 )
);
}
}
$srcs = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );
$files = $request->get_file_params();
foreach ( $srcs as $src ) {
// Check that each src is a non-empty string.
$src = ltrim( $src );
if ( empty( $src ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: Font face source parameter name: "font_face_settings[src]". */
sprintf( __( '%s values must be non-empty strings.' ), 'font_face_settings[src]' ),
array( 'status' => 400 )
);
}
// Check that srcs are valid URLs or file references.
if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Font face source parameter name: "font_face_settings[src]", 2: The invalid src value. */
sprintf( __( '%1$s value "%2$s" must be a valid URL or file reference.' ), 'font_face_settings[src]', $src ),
array( 'status' => 400 )
);
}
}
// Check that each file in the request references a src in the settings.
foreach ( array_keys( $files ) as $file ) {
if ( ! in_array( $file, $srcs, true ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: 1: File key (e.g. "file-0") in the request data, 2: Font face source parameter name: "font_face_settings[src]". */
sprintf( __( 'File %1$s must be used in %2$s.' ), $file, 'font_face_settings[src]' ),
array( 'status' => 400 )
);
}
}
return true;
}
/**
* Sanitizes the font face settings when creating a font face.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font face settings.
* @return array Decoded and sanitized array of font face settings.
*/
public function sanitize_font_face_settings( $value ) {
// Settings arrive as stringified JSON, since this is a multipart/form-data request.
$settings = json_decode( $value, true );
$schema = $this->get_item_schema()['properties']['font_face_settings']['properties'];
// Sanitize settings based on callbacks in the schema.
foreach ( $settings as $key => $value ) {
$sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
$settings[ $key ] = call_user_func( $sanitize_callback, $value );
}
return $settings;
}
/**
* Retrieves a collection of font faces within the parent font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
if ( is_wp_error( $font_family ) ) {
return $font_family;
}
return parent::get_items( $request );
}
/**
* Retrieves a single font face within the parent font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
// Check that the font face has a valid parent font family.
$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
if ( is_wp_error( $font_family ) ) {
return $font_family;
}
if ( (int) $font_family->ID !== (int) $post->post_parent ) {
return new WP_Error(
'rest_font_face_parent_id_mismatch',
/* translators: %d: A post id. */
sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
array( 'status' => 404 )
);
}
return parent::get_item( $request );
}
/**
* Creates a font face for the parent font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
if ( is_wp_error( $font_family ) ) {
return $font_family;
}
// Settings have already been decoded by ::sanitize_font_face_settings().
$settings = $request->get_param( 'font_face_settings' );
$file_params = $request->get_file_params();
// Check that the necessary font face properties are unique.
$query = new WP_Query(
array(
'post_type' => $this->post_type,
'posts_per_page' => 1,
'title' => WP_Font_Utils::get_font_face_slug( $settings ),
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! empty( $query->posts ) ) {
return new WP_Error(
'rest_duplicate_font_face',
__( 'A font face matching those settings already exists.' ),
array( 'status' => 400 )
);
}
// Move the uploaded font asset from the temp folder to the fonts directory.
if ( ! function_exists( 'wp_handle_upload' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$srcs = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src'];
$processed_srcs = array();
$font_file_meta = array();
foreach ( $srcs as $src ) {
// If src not a file reference, use it as is.
if ( ! isset( $file_params[ $src ] ) ) {
$processed_srcs[] = $src;
continue;
}
$file = $file_params[ $src ];
$font_file = $this->handle_font_file_upload( $file );
if ( is_wp_error( $font_file ) ) {
return $font_file;
}
$processed_srcs[] = $font_file['url'];
$font_file_meta[] = $this->relative_fonts_path( $font_file['file'] );
}
// Store the updated settings for prepare_item_for_database to use.
$settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs;
$request->set_param( 'font_face_settings', $settings );
// Ensure that $settings data is slashed, so values with quotes are escaped.
// WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content.
$font_face_post = parent::create_item( $request );
if ( is_wp_error( $font_face_post ) ) {
return $font_face_post;
}
$font_face_id = $font_face_post->data['id'];
foreach ( $font_file_meta as $font_file_path ) {
add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path );
}
return $font_face_post;
}
/**
* Deletes a single font face.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
if ( is_wp_error( $font_family ) ) {
return $font_family;
}
if ( (int) $font_family->ID !== (int) $post->post_parent ) {
return new WP_Error(
'rest_font_face_parent_id_mismatch',
/* translators: %d: A post id. */
sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
array( 'status' => 404 )
);
}
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for font faces.
if ( ! $force ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
array( 'status' => 501 )
);
}
return parent::delete_item( $request );
}
/**
* Prepares a single font face output for response.
*
* @since 6.5.0
*
* @param WP_Post $item Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $item->ID;
}
if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
$data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
}
if ( rest_is_field_included( 'parent', $fields ) ) {
$data['parent'] = $item->post_parent;
}
if ( rest_is_field_included( 'font_face_settings', $fields ) ) {
$data['font_face_settings'] = $this->get_settings_from_post( $item );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $item );
$response->add_links( $links );
}
/**
* Filters the font face data for a REST API response.
*
* @since 6.5.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Font face post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request );
}
/**
* Retrieves the post's schema, conforming to JSON Schema.
*
* @since 6.5.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
// Base properties for every Post.
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the post.', 'default' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'theme_json_version' => array(
'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
'type' => 'integer',
'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
'minimum' => 2,
'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
'context' => array( 'view', 'edit', 'embed' ),
),
'parent' => array(
'description' => __( 'The ID for the parent font family of the font face.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
// Font face settings come directly from theme.json schema
// See https://schemas.wp.org/trunk/theme.json
'font_face_settings' => array(
'description' => __( 'font-face declaration in theme.json format.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'properties' => array(
'fontFamily' => array(
'description' => __( 'CSS font-family value.' ),
'type' => 'string',
'default' => '',
'arg_options' => array(
'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
),
),
'fontStyle' => array(
'description' => __( 'CSS font-style value.' ),
'type' => 'string',
'default' => 'normal',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontWeight' => array(
'description' => __( 'List of available font weights, separated by a space.' ),
'default' => '400',
// Changed from `oneOf` to avoid errors from loose type checking.
// e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check.
'type' => array( 'string', 'integer' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontDisplay' => array(
'description' => __( 'CSS font-display value.' ),
'type' => 'string',
'default' => 'fallback',
'enum' => array(
'auto',
'block',
'fallback',
'swap',
'optional',
),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'src' => array(
'description' => __( 'Paths or URLs to the font files.' ),
// Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array,
// and causing a "matches more than one of the expected formats" error.
'anyOf' => array(
array(
'type' => 'string',
),
array(
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
),
'default' => array(),
'arg_options' => array(
'sanitize_callback' => function ( $value ) {
return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value );
},
),
),
'fontStretch' => array(
'description' => __( 'CSS font-stretch value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'ascentOverride' => array(
'description' => __( 'CSS ascent-override value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'descentOverride' => array(
'description' => __( 'CSS descent-override value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontVariant' => array(
'description' => __( 'CSS font-variant value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontFeatureSettings' => array(
'description' => __( 'CSS font-feature-settings value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'fontVariationSettings' => array(
'description' => __( 'CSS font-variation-settings value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'lineGapOverride' => array(
'description' => __( 'CSS line-gap-override value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'sizeAdjust' => array(
'description' => __( 'CSS size-adjust value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'unicodeRange' => array(
'description' => __( 'CSS unicode-range value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'preview' => array(
'description' => __( 'URL to a preview image of the font face.' ),
'type' => 'string',
'format' => 'uri',
'default' => '',
'arg_options' => array(
'sanitize_callback' => 'sanitize_url',
),
),
),
'required' => array( 'fontFamily', 'src' ),
'additionalProperties' => false,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @since 6.5.0
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {
$schema = parent::get_public_item_schema();
// Also remove `arg_options' from child font_family_settings properties, since the parent
// controller only handles the top level properties.
foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) {
unset( $property['arg_options'] );
}
return $schema;
}
/**
* Retrieves the query params for the font face collection.
*
* @since 6.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
// Remove unneeded params.
unset(
$query_params['after'],
$query_params['modified_after'],
$query_params['before'],
$query_params['modified_before'],
$query_params['search'],
$query_params['search_columns'],
$query_params['slug'],
$query_params['status']
);
$query_params['orderby']['default'] = 'id';
$query_params['orderby']['enum'] = array( 'id', 'include' );
/**
* Filters collection parameters for the font face controller.
*
* @since 6.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_wp_font_face_collection_params', $query_params );
}
/**
* Get the params used when creating a new font face.
*
* @since 6.5.0
*
* @return array Font face create arguments.
*/
public function get_create_params() {
$properties = $this->get_item_schema()['properties'];
return array(
'theme_json_version' => $properties['theme_json_version'],
// When creating, font_face_settings is stringified JSON, to work with multipart/form-data used
// when uploading font files.
'font_face_settings' => array(
'description' => __( 'font-face declaration in theme.json format, encoded as a string.' ),
'type' => 'string',
'required' => true,
'validate_callback' => array( $this, 'validate_create_font_face_settings' ),
'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ),
),
);
}
/**
* Get the parent font family, if the ID is valid.
*
* @since 6.5.0
*
* @param int $font_family_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent_font_family_post( $font_family_id ) {
$error = new WP_Error(
'rest_post_invalid_parent',
__( 'Invalid post parent ID.', 'default' ),
array( 'status' => 404 )
);
if ( (int) $font_family_id <= 0 ) {
return $error;
}
$font_family_post = get_post( (int) $font_family_id );
if ( empty( $font_family_post ) || empty( $font_family_post->ID )
|| 'wp_font_family' !== $font_family_post->post_type
) {
return $error;
}
return $font_family_post;
}
/**
* Prepares links for the request.
*
* @since 6.5.0
*
* @param WP_Post $post Post object.
* @return array Links for the given post.
*/
protected function prepare_links( $post ) {
// Entity meta.
return array(
'self' => array(
'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ),
),
'collection' => array(
'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ),
),
'parent' => array(
'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ),
),
);
}
/**
* Prepares a single font face post for creation.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass Post object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass();
// Settings have already been decoded by ::sanitize_font_face_settings().
$settings = $request->get_param( 'font_face_settings' );
// Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting,
// which may contain multibyte characters.
$title = WP_Font_Utils::get_font_face_slug( $settings );
$prepared_post->post_type = $this->post_type;
$prepared_post->post_parent = $request['font_family_id'];
$prepared_post->post_status = 'publish';
$prepared_post->post_title = $title;
$prepared_post->post_name = sanitize_title( $title );
$prepared_post->post_content = wp_json_encode( $settings );
return $prepared_post;
}
/**
* Sanitizes a single src value for a font face.
*
* @since 6.5.0
*
* @param string $value Font face src that is a URL or the key for a $_FILES array item.
* @return string Sanitized value.
*/
protected function sanitize_src( $value ) {
$value = ltrim( $value );
return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value );
}
/**
* Handles the upload of a font file using wp_handle_upload().
*
* @since 6.5.0
*
* @param array $file Single file item from $_FILES.
* @return array|WP_Error Array containing uploaded file attributes on success, or WP_Error object on failure.
*/
protected function handle_font_file_upload( $file ) {
add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
// Filter the upload directory to return the fonts directory.
add_filter( 'upload_dir', '_wp_filter_font_directory' );
$overrides = array(
'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
// Not testing a form submission.
'test_form' => false,
// Only allow uploading font files for this request.
'mimes' => WP_Font_Utils::get_allowed_font_mime_types(),
);
// Bypasses is_uploaded_file() when running unit tests.
if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
$overrides['action'] = 'wp_handle_mock_upload';
}
$uploaded_file = wp_handle_upload( $file, $overrides );
remove_filter( 'upload_dir', '_wp_filter_font_directory' );
remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
return $uploaded_file;
}
/**
* Handles file upload error.
*
* @since 6.5.0
*
* @param array $file File upload data.
* @param string $message Error message from wp_handle_upload().
* @return WP_Error WP_Error object.
*/
public function handle_font_file_upload_error( $file, $message ) {
$status = 500;
$code = 'rest_font_upload_unknown_error';
if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) {
$status = 400;
$code = 'rest_font_upload_invalid_file_type';
}
return new WP_Error( $code, $message, array( 'status' => $status ) );
}
/**
* Returns relative path to an uploaded font file.
*
* The path is relative to the current fonts directory.
*
* @since 6.5.0
* @access private
*
* @param string $path Full path to the file.
* @return string Relative path on success, unchanged path on failure.
*/
protected function relative_fonts_path( $path ) {
$new_path = $path;
$fonts_dir = wp_get_font_dir();
if ( str_starts_with( $new_path, $fonts_dir['basedir'] ) ) {
$new_path = str_replace( $fonts_dir['basedir'], '', $new_path );
$new_path = ltrim( $new_path, '/' );
}
return $new_path;
}
/**
* Gets the font face's settings from the post.
*
* @since 6.5.0
*
* @param WP_Post $post Font face post object.
* @return array Font face settings array.
*/
protected function get_settings_from_post( $post ) {
$settings = json_decode( $post->post_content, true );
$properties = $this->get_item_schema()['properties']['font_face_settings']['properties'];
// Provide required, empty settings if needed.
if ( null === $settings ) {
$settings = array(
'fontFamily' => '',
'src' => array(),
);
}
// Only return the properties defined in the schema.
return array_intersect_key( $settings, $properties );
}
}
endpoints/class-wp-rest-font-families-controller.php 0000644 00000042152 15153067731 0016714 0 ustar 00 <?php
/**
* REST API: WP_REST_Font_Families_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.5.0
*/
/**
* Font Families Controller class.
*
* @since 6.5.0
*/
class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller {
/**
* The latest version of theme.json schema supported by the controller.
*
* @since 6.5.0
* @var int
*/
const LATEST_THEME_JSON_VERSION_SUPPORTED = 2;
/**
* Whether the controller supports batching.
*
* @since 6.5.0
* @var false
*/
protected $allow_batch = false;
/**
* Checks if a given request has access to font families.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
if ( ! current_user_can( $post_type->cap->read ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to access font families.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a given request has access to a font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( ! current_user_can( 'read_post', $post->ID ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to access this font family.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Validates settings when creating or updating a font family.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font family settings.
* @param WP_REST_Request $request Request object.
* @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
*/
public function validate_font_family_settings( $value, $request ) {
$settings = json_decode( $value, true );
// Check settings string is valid JSON.
if ( null === $settings ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: Parameter name: "font_family_settings". */
sprintf( __( '%s parameter must be a valid JSON string.' ), 'font_family_settings' ),
array( 'status' => 400 )
);
}
$schema = $this->get_item_schema()['properties']['font_family_settings'];
$required = $schema['required'];
if ( isset( $request['id'] ) ) {
// Allow sending individual properties if we are updating an existing font family.
unset( $schema['required'] );
// But don't allow updating the slug, since it is used as a unique identifier.
if ( isset( $settings['slug'] ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: Name of parameter being updated: font_family_settings[slug]". */
sprintf( __( '%s cannot be updated.' ), 'font_family_settings[slug]' ),
array( 'status' => 400 )
);
}
}
// Check that the font face settings match the theme.json schema.
$has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' );
if ( is_wp_error( $has_valid_settings ) ) {
$has_valid_settings->add_data( array( 'status' => 400 ) );
return $has_valid_settings;
}
// Check that none of the required settings are empty values.
foreach ( $required as $key ) {
if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
return new WP_Error(
'rest_invalid_param',
/* translators: %s: Name of the empty font family setting parameter, e.g. "font_family_settings[slug]". */
sprintf( __( '%s cannot be empty.' ), "font_family_settings[ $key ]" ),
array( 'status' => 400 )
);
}
}
return true;
}
/**
* Sanitizes the font family settings when creating or updating a font family.
*
* @since 6.5.0
*
* @param string $value Encoded JSON string of font family settings.
* @return array Decoded array of font family settings.
*/
public function sanitize_font_family_settings( $value ) {
// Settings arrive as stringified JSON, since this is a multipart/form-data request.
$settings = json_decode( $value, true );
$schema = $this->get_item_schema()['properties']['font_family_settings']['properties'];
// Sanitize settings based on callbacks in the schema.
foreach ( $settings as $key => $value ) {
$sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
$settings[ $key ] = call_user_func( $sanitize_callback, $value );
}
return $settings;
}
/**
* Creates a single font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$settings = $request->get_param( 'font_family_settings' );
// Check that the font family slug is unique.
$query = new WP_Query(
array(
'post_type' => $this->post_type,
'posts_per_page' => 1,
'name' => $settings['slug'],
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
if ( ! empty( $query->posts ) ) {
return new WP_Error(
'rest_duplicate_font_family',
/* translators: %s: Font family slug. */
sprintf( __( 'A font family with slug "%s" already exists.' ), $settings['slug'] ),
array( 'status' => 400 )
);
}
return parent::create_item( $request );
}
/**
* Deletes a single font family.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for font families.
if ( ! $force ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
array( 'status' => 501 )
);
}
return parent::delete_item( $request );
}
/**
* Prepares a single font family output for response.
*
* @since 6.5.0
*
* @param WP_Post $item Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $item->ID;
}
if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
$data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
}
if ( rest_is_field_included( 'font_faces', $fields ) ) {
$data['font_faces'] = $this->get_font_face_ids( $item->ID );
}
if ( rest_is_field_included( 'font_family_settings', $fields ) ) {
$data['font_family_settings'] = $this->get_settings_from_post( $item );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) ) {
$links = $this->prepare_links( $item );
$response->add_links( $links );
}
/**
* Filters the font family data for a REST API response.
*
* @since 6.5.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Font family post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request );
}
/**
* Retrieves the post's schema, conforming to JSON Schema.
*
* @since 6.5.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
// Base properties for every Post.
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the post.', 'default' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'theme_json_version' => array(
'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
'type' => 'integer',
'default' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
'minimum' => 2,
'maximum' => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
'context' => array( 'view', 'edit', 'embed' ),
),
'font_faces' => array(
'description' => __( 'The IDs of the child font faces in the font family.' ),
'type' => 'array',
'context' => array( 'view', 'edit', 'embed' ),
'items' => array(
'type' => 'integer',
),
),
// Font family settings come directly from theme.json schema
// See https://schemas.wp.org/trunk/theme.json
'font_family_settings' => array(
'description' => __( 'font-face definition in theme.json format.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'properties' => array(
'name' => array(
'description' => __( 'Name of the font family preset, translatable.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'Kebab-case unique identifier for the font family preset.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'fontFamily' => array(
'description' => __( 'CSS font-family value.' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
),
),
'preview' => array(
'description' => __( 'URL to a preview image of the font family.' ),
'type' => 'string',
'format' => 'uri',
'default' => '',
'arg_options' => array(
'sanitize_callback' => 'sanitize_url',
),
),
),
'required' => array( 'name', 'slug', 'fontFamily' ),
'additionalProperties' => false,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @since 6.5.0
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {
$schema = parent::get_public_item_schema();
// Also remove `arg_options' from child font_family_settings properties, since the parent
// controller only handles the top level properties.
foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) {
unset( $property['arg_options'] );
}
return $schema;
}
/**
* Retrieves the query params for the font family collection.
*
* @since 6.5.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
// Remove unneeded params.
unset(
$query_params['after'],
$query_params['modified_after'],
$query_params['before'],
$query_params['modified_before'],
$query_params['search'],
$query_params['search_columns'],
$query_params['status']
);
$query_params['orderby']['default'] = 'id';
$query_params['orderby']['enum'] = array( 'id', 'include' );
/**
* Filters collection parameters for the font family controller.
*
* @since 6.5.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_wp_font_family_collection_params', $query_params );
}
/**
* Get the arguments used when creating or updating a font family.
*
* @since 6.5.0
*
* @return array Font family create/edit arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) {
$properties = $this->get_item_schema()['properties'];
return array(
'theme_json_version' => $properties['theme_json_version'],
// When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data.
// Font families don't currently support file uploads, but may accept preview files in the future.
'font_family_settings' => array(
'description' => __( 'font-family declaration in theme.json format, encoded as a string.' ),
'type' => 'string',
'required' => true,
'validate_callback' => array( $this, 'validate_font_family_settings' ),
'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ),
),
);
}
return parent::get_endpoint_args_for_item_schema( $method );
}
/**
* Get the child font face post IDs.
*
* @since 6.5.0
*
* @param int $font_family_id Font family post ID.
* @return int[] Array of child font face post IDs.
*/
protected function get_font_face_ids( $font_family_id ) {
$query = new WP_Query(
array(
'fields' => 'ids',
'post_parent' => $font_family_id,
'post_type' => 'wp_font_face',
'posts_per_page' => 99,
'order' => 'ASC',
'orderby' => 'id',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
return $query->posts;
}
/**
* Prepares font family links for the request.
*
* @since 6.5.0
*
* @param WP_Post $post Post object.
* @return array Links for the given post.
*/
protected function prepare_links( $post ) {
// Entity meta.
$links = parent::prepare_links( $post );
return array(
'self' => $links['self'],
'collection' => $links['collection'],
'font_faces' => $this->prepare_font_face_links( $post->ID ),
);
}
/**
* Prepares child font face links for the request.
*
* @param int $font_family_id Font family post ID.
* @return array Links for the child font face posts.
*/
protected function prepare_font_face_links( $font_family_id ) {
$font_face_ids = $this->get_font_face_ids( $font_family_id );
$links = array();
foreach ( $font_face_ids as $font_face_id ) {
$links[] = array(
'embeddable' => true,
'href' => rest_url( sprintf( '%s/%s/%s/font-faces/%s', $this->namespace, $this->rest_base, $font_family_id, $font_face_id ) ),
);
}
return $links;
}
/**
* Prepares a single font family post for create or update.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass();
// Settings have already been decoded by ::sanitize_font_family_settings().
$settings = $request->get_param( 'font_family_settings' );
// This is an update and we merge with the existing font family.
if ( isset( $request['id'] ) ) {
$existing_post = $this->get_post( $request['id'] );
if ( is_wp_error( $existing_post ) ) {
return $existing_post;
}
$prepared_post->ID = $existing_post->ID;
$existing_settings = $this->get_settings_from_post( $existing_post );
$settings = array_merge( $existing_settings, $settings );
}
$prepared_post->post_type = $this->post_type;
$prepared_post->post_status = 'publish';
$prepared_post->post_title = $settings['name'];
$prepared_post->post_name = sanitize_title( $settings['slug'] );
// Remove duplicate information from settings.
unset( $settings['name'] );
unset( $settings['slug'] );
$prepared_post->post_content = wp_json_encode( $settings );
return $prepared_post;
}
/**
* Gets the font family's settings from the post.
*
* @since 6.5.0
*
* @param WP_Post $post Font family post object.
* @return array Font family settings array.
*/
protected function get_settings_from_post( $post ) {
$settings_json = json_decode( $post->post_content, true );
// Default to empty strings if the settings are missing.
return array(
'name' => isset( $post->post_title ) && $post->post_title ? $post->post_title : '',
'slug' => isset( $post->post_name ) && $post->post_name ? $post->post_name : '',
'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '',
'preview' => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '',
);
}
}
endpoints/class-wp-rest-global-styles-controller.php 0000644 00000050170 15153067731 0016737 0 ustar 00 <?php
/**
* REST API: WP_REST_Global_Styles_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.9.0
*/
/**
* Base Global Styles REST API Controller.
*/
class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
/**
* Post type.
*
* @since 5.9.0
* @var string
*/
protected $post_type;
/**
* Constructor.
* @since 5.9.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'global-styles';
$this->post_type = 'wp_global_styles';
}
/**
* Registers the controllers routes.
*
* @since 5.9.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_theme_items' ),
'permission_callback' => array( $this, 'get_theme_items_permissions_check' ),
'args' => array(
'stylesheet' => array(
'description' => __( 'The theme identifier' ),
'type' => 'string',
),
),
),
)
);
// List themes global styles.
register_rest_route(
$this->namespace,
// The route.
sprintf(
'/%s/themes/(?P<stylesheet>%s)',
$this->rest_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
),
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_theme_item' ),
'permission_callback' => array( $this, 'get_theme_item_permissions_check' ),
'args' => array(
'stylesheet' => array(
'description' => __( 'The theme identifier' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
),
),
),
)
);
// Lists/updates a single global style variation based on the given id.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\/\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Sanitize the global styles ID or stylesheet to decode endpoint.
* For example, `wp/v2/global-styles/twentytwentytwo%200.4.0`
* would be decoded to `twentytwentytwo 0.4.0`.
*
* @since 5.9.0
*
* @param string $id_or_stylesheet Global styles ID or stylesheet.
* @return string Sanitized global styles ID or stylesheet.
*/
public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
return urldecode( $id_or_stylesheet );
}
/**
* Get the post, if the ID is valid.
*
* @since 5.9.0
*
* @param int $id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_post( $id ) {
$error = new WP_Error(
'rest_global_styles_not_found',
__( 'No global styles config exist with that id.' ),
array( 'status' => 404 )
);
$id = (int) $id;
if ( $id <= 0 ) {
return $error;
}
$post = get_post( $id );
if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
return $error;
}
return $post;
}
/**
* Checks if a given request has access to read a single global style.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit this global style.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! $this->check_read_permission( $post ) ) {
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view this global style.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a global style can be read.
*
* @since 5.9.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be read.
*/
protected function check_read_permission( $post ) {
return current_user_can( 'read_post', $post->ID );
}
/**
* Returns the given global styles config.
*
* @since 5.9.0
*
* @param WP_REST_Request $request The request instance.
*
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
return $this->prepare_item_for_response( $post, $request );
}
/**
* Checks if a given request has access to write a single global styles config.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this global style.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks if a global style can be edited.
*
* @since 5.9.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be edited.
*/
protected function check_update_permission( $post ) {
return current_user_can( 'edit_post', $post->ID );
}
/**
* Updates a single global style config.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$post_before = $this->get_post( $request['id'] );
if ( is_wp_error( $post_before ) ) {
return $post_before;
}
$changes = $this->prepare_item_for_database( $request );
if ( is_wp_error( $changes ) ) {
return $changes;
}
$result = wp_update_post( wp_slash( (array) $changes ), true, false );
if ( is_wp_error( $result ) ) {
return $result;
}
$post = get_post( $request['id'] );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
wp_after_insert_post( $post, true, $post_before );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/**
* Prepares a single global styles config for update.
*
* @since 5.9.0
* @since 6.2.0 Added validation of styles.css property.
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
*/
protected function prepare_item_for_database( $request ) {
$changes = new stdClass();
$changes->ID = $request['id'];
$post = get_post( $request['id'] );
$existing_config = array();
if ( $post ) {
$existing_config = json_decode( $post->post_content, true );
$json_decoding_error = json_last_error();
if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
! $existing_config['isGlobalStylesUserThemeJSON'] ) {
$existing_config = array();
}
}
if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
if ( isset( $request['styles']['css'] ) ) {
$css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
if ( is_wp_error( $css_validation_result ) ) {
return $css_validation_result;
}
}
$config['styles'] = $request['styles'];
} elseif ( isset( $existing_config['styles'] ) ) {
$config['styles'] = $existing_config['styles'];
}
if ( isset( $request['settings'] ) ) {
$config['settings'] = $request['settings'];
} elseif ( isset( $existing_config['settings'] ) ) {
$config['settings'] = $existing_config['settings'];
}
$config['isGlobalStylesUserThemeJSON'] = true;
$config['version'] = WP_Theme_JSON::LATEST_SCHEMA;
$changes->post_content = wp_json_encode( $config );
}
// Post title.
if ( isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$changes->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$changes->post_title = $request['title']['raw'];
}
}
return $changes;
}
/**
* Prepare a global styles config output for response.
*
* @since 5.9.0
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) {
$raw_config = json_decode( $post->post_content, true );
$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
$config = array();
if ( $is_global_styles_user_theme_json ) {
$config = ( new WP_Theme_JSON( $raw_config, 'custom' ) )->get_raw_data();
}
// Base fields for every post.
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}
if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
$data['title']['rendered'] = get_the_title( $post->ID );
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}
if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
}
if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $post->ID );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions();
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}
return $response;
}
/**
* Prepares links for the request.
*
* @since 5.9.0
* @since 6.3.0 Adds revisions count and rest URL href to version-history.
*
* @param integer $id ID.
* @return array Links for the given post.
*/
protected function prepare_links( $id ) {
$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $id ),
),
);
if ( post_type_supports( $this->post_type, 'revisions' ) ) {
$revisions = wp_get_latest_revision_id_and_total_count( $id );
$revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
$revisions_base = sprintf( '/%s/%d/revisions', $base, $id );
$links['version-history'] = array(
'href' => rest_url( $revisions_base ),
'count' => $revisions_count,
);
}
return $links;
}
/**
* Get the link relations available for the post and current user.
*
* @since 5.9.0
* @since 6.2.0 Added 'edit-css' action.
*
* @return array List of link relations.
*/
protected function get_available_actions() {
$rels = array();
$post_type = get_post_type_object( $this->post_type );
if ( current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-publish';
}
if ( current_user_can( 'edit_css' ) ) {
$rels[] = 'https://api.w.org/action-edit-css';
}
return $rels;
}
/**
* Overwrites the default protected title format.
*
* By default, WordPress will show password protected posts with a title of
* "Protected: %s", as the REST API communicates the protected status of a post
* in a machine readable format, we remove the "Protected: " prefix.
*
* @since 5.9.0
*
* @return string Protected title format.
*/
public function protected_title_format() {
return '%s';
}
/**
* Retrieves the query params for the global styles collection.
*
* @since 5.9.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array();
}
/**
* Retrieves the global styles type' schema, conforming to JSON Schema.
*
* @since 5.9.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID of global styles config.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'styles' => array(
'description' => __( 'Global styles.' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
'settings' => array(
'description' => __( 'Global settings.' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
'title' => array(
'description' => __( 'Title of the global styles variation.' ),
'type' => array( 'object', 'string' ),
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the global styles variation, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'rendered' => array(
'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Checks if a given request has access to read a single theme global styles config.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_theme_item_permissions_check( $request ) {
/*
* Verify if the current user has edit_theme_options capability.
* This capability is required to edit/view/delete templates.
*/
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_global_styles',
__( 'Sorry, you are not allowed to access the global styles on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Returns the given theme global styles config.
*
* @since 5.9.0
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
*/
public function get_theme_item( $request ) {
if ( get_stylesheet() !== $request['stylesheet'] ) {
// This endpoint only supports the active theme for now.
return new WP_Error(
'rest_theme_not_found',
__( 'Theme not found.' ),
array( 'status' => 404 )
);
}
$theme = WP_Theme_JSON_Resolver::get_merged_data( 'theme' );
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = $theme->get_settings();
}
if ( rest_is_field_included( 'styles', $fields ) ) {
$raw_data = $theme->get_raw_data();
$data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
),
);
$response->add_links( $links );
}
return $response;
}
/**
* Checks if a given request has access to read a single theme global styles config.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_theme_items_permissions_check( $request ) {
/*
* Verify if the current user has edit_theme_options capability.
* This capability is required to edit/view/delete templates.
*/
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_global_styles',
__( 'Sorry, you are not allowed to access the global styles on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Returns the given theme global styles variations.
*
* @since 6.0.0
* @since 6.2.0 Returns parent theme variations, if they exist.
*
* @param WP_REST_Request $request The request instance.
*
* @return WP_REST_Response|WP_Error
*/
public function get_theme_items( $request ) {
if ( get_stylesheet() !== $request['stylesheet'] ) {
// This endpoint only supports the active theme for now.
return new WP_Error(
'rest_theme_not_found',
__( 'Theme not found.' ),
array( 'status' => 404 )
);
}
$variations = WP_Theme_JSON_Resolver::get_style_variations();
return rest_ensure_response( $variations );
}
/**
* Validate style.css as valid CSS.
*
* Currently just checks for invalid markup.
*
* @since 6.2.0
* @since 6.4.0 Changed method visibility to protected.
*
* @param string $css CSS to validate.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
*/
protected function validate_custom_css( $css ) {
if ( preg_match( '#</?\w+#', $css ) ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
__( 'Markup is not allowed in CSS.' ),
array( 'status' => 400 )
);
}
return true;
}
}
endpoints/class-wp-rest-global-styles-revisions-controller.php 0000644 00000040117 15153067731 0020756 0 ustar 00 <?php
/**
* REST API: WP_REST_Global_Styles_Revisions_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.3.0
*/
/**
* Core class used to access global styles revisions via the REST API.
*
* @since 6.3.0
*
* @see WP_REST_Controller
*/
class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Controller {
/**
* Parent post type.
*
* @since 6.3.0
* @var string
*/
protected $parent_post_type;
/**
* The base of the parent controller's route.
*
* @since 6.3.0
* @var string
*/
protected $parent_base;
/**
* Constructor.
*
* @since 6.3.0
*/
public function __construct() {
$this->parent_post_type = 'wp_global_styles';
$this->rest_base = 'revisions';
$this->parent_base = 'global-styles';
$this->namespace = 'wp/v2';
}
/**
* Registers the controller's routes.
*
* @since 6.3.0
* @since 6.5.0 Added route to fetch individual global styles revisions.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base,
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the global styles revision.' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the global styles revision.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Retrieves the query params for collections.
*
* Inherits from WP_REST_Controller::get_collection_params(),
* also reflects changes to return value WP_REST_Revisions_Controller::get_collection_params().
*
* @since 6.3.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$collection_params = parent::get_collection_params();
$collection_params['context']['default'] = 'view';
$collection_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
unset( $collection_params['search'] );
unset( $collection_params['per_page']['default'] );
return $collection_params;
}
/**
* Returns decoded JSON from post content string,
* or a 404 if not found.
*
* @since 6.3.0
*
* @param string $raw_json Encoded JSON from global styles custom post content.
* @return Array|WP_Error
*/
protected function get_decoded_global_styles_json( $raw_json ) {
$decoded_json = json_decode( $raw_json, true );
if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) {
return $decoded_json;
}
return new WP_Error(
'rest_global_styles_not_found',
__( 'Cannot find user global styles revisions.' ),
array( 'status' => 404 )
);
}
/**
* Returns paginated revisions of the given global styles config custom post type.
*
* The bulk of the body is taken from WP_REST_Revisions_Controller->get_items,
* but global styles does not require as many parameters.
*
* @since 6.3.0
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
$global_styles_config = $this->get_decoded_global_styles_json( $parent->post_content );
if ( is_wp_error( $global_styles_config ) ) {
return $global_styles_config;
}
if ( wp_revisions_enabled( $parent ) ) {
$registered = $this->get_collection_params();
$query_args = array(
'post_parent' => $parent->ID,
'post_type' => 'revision',
'post_status' => 'inherit',
'posts_per_page' => -1,
'orderby' => 'date ID',
'order' => 'DESC',
);
$parameter_mappings = array(
'offset' => 'offset',
'page' => 'paged',
'per_page' => 'posts_per_page',
);
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$query_args[ $wp_param ] = $request[ $api_param ];
}
}
$revisions_query = new WP_Query();
$revisions = $revisions_query->query( $query_args );
$offset = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0;
$page = (int) $query_args['paged'];
$total_revisions = $revisions_query->found_posts;
if ( $total_revisions < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'], $query_args['offset'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_revisions = $count_query->found_posts;
}
if ( $revisions_query->query_vars['posts_per_page'] > 0 ) {
$max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] );
} else {
$max_pages = $total_revisions > 0 ? 1 : 0;
}
if ( $total_revisions > 0 ) {
if ( $offset >= $total_revisions ) {
return new WP_Error(
'rest_revision_invalid_offset_number',
__( 'The offset number requested is larger than or equal to the number of available revisions.' ),
array( 'status' => 400 )
);
} elseif ( ! $offset && $page > $max_pages ) {
return new WP_Error(
'rest_revision_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
}
} else {
$revisions = array();
$total_revisions = 0;
$max_pages = 0;
$page = (int) $request['page'];
}
$response = array();
foreach ( $revisions as $revision ) {
$data = $this->prepare_item_for_response( $revision, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $response );
$response->header( 'X-WP-Total', (int) $total_revisions );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$request_params = $request->get_query_params();
$base_path = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) );
$base = add_query_arg( urlencode_deep( $request_params ), $base_path );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Retrieves one global styles revision from the collection.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
$revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) {
return $revision;
}
$response = $this->prepare_item_for_response( $revision, $request );
return rest_ensure_response( $response );
}
/**
* Gets the global styles revision, if the ID is valid.
*
* @since 6.5.0
*
* @param int $id Supplied ID.
* @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
*/
protected function get_revision( $id ) {
$error = new WP_Error(
'rest_post_invalid_id',
__( 'Invalid global styles revision ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$revision = get_post( (int) $id );
if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
return $error;
}
return $revision;
}
/**
* Checks the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* Duplicate of WP_REST_Revisions_Controller::prepare_date_response.
*
* @since 6.3.0
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional. Local publication time. Default null.
* @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
*/
protected function prepare_date_response( $date_gmt, $date = null ) {
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
return mysql_to_rfc3339( $date_gmt );
}
/**
* Prepares the revision for the REST response.
*
* @since 6.3.0
*
* @param WP_Post $post Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object.
*/
public function prepare_item_for_response( $post, $request ) {
$parent = $this->get_parent( $request['parent'] );
$global_styles_config = $this->get_decoded_global_styles_json( $post->post_content );
if ( is_wp_error( $global_styles_config ) ) {
return $global_styles_config;
}
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) {
$global_styles_config = ( new WP_Theme_JSON( $global_styles_config, 'custom' ) )->get_raw_data();
if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
}
if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass();
}
}
if ( rest_is_field_included( 'author', $fields ) ) {
$data['author'] = (int) $post->post_author;
}
if ( rest_is_field_included( 'date', $fields ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}
if ( rest_is_field_included( 'date_gmt', $fields ) ) {
$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
}
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = (int) $post->ID;
}
if ( rest_is_field_included( 'modified', $fields ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}
if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
}
if ( rest_is_field_included( 'parent', $fields ) ) {
$data['parent'] = (int) $parent->ID;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
return rest_ensure_response( $data );
}
/**
* Retrieves the revision's schema, conforming to JSON Schema.
*
* @since 6.3.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => "{$this->parent_post_type}-revision",
'type' => 'object',
// Base properties for every revision.
'properties' => array(
/*
* Adds settings and styles from the WP_REST_Revisions_Controller item fields.
* Leaves out GUID as global styles shouldn't be accessible via URL.
*/
'author' => array(
'description' => __( 'The ID for the author of the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'date' => array(
'description' => __( "The date the revision was published, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the revision was published, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'id' => array(
'description' => __( 'Unique identifier for the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'modified' => array(
'description' => __( "The date the revision was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'modified_gmt' => array(
'description' => __( 'The date the revision was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'parent' => array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
// Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema.
'styles' => array(
'description' => __( 'Global styles.' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
'settings' => array(
'description' => __( 'Global settings.' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Checks if a given request has access to read a single global style.
*
* @since 6.3.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_parent( $request['parent'] );
if ( is_wp_error( $post ) ) {
return $post;
}
/*
* The same check as WP_REST_Global_Styles_Controller::get_item_permissions_check.
*/
if ( ! current_user_can( 'read_post', $post->ID ) ) {
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view revisions for this global style.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Gets the parent post, if the ID is valid.
*
* Duplicate of WP_REST_Revisions_Controller::get_parent.
*
* @since 6.3.0
*
* @param int $parent_post_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent( $parent_post_id ) {
$error = new WP_Error(
'rest_post_invalid_parent',
__( 'Invalid post parent ID.' ),
array( 'status' => 404 )
);
if ( (int) $parent_post_id <= 0 ) {
return $error;
}
$parent_post = get_post( (int) $parent_post_id );
if ( empty( $parent_post ) || empty( $parent_post->ID )
|| $this->parent_post_type !== $parent_post->post_type
) {
return $error;
}
return $parent_post;
}
}
endpoints/class-wp-rest-menu-items-controller.php 0000644 00000077265 15153067731 0016257 0 ustar 00 <?php
/**
* REST API: WP_REST_Menu_Items_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.9.0
*/
/**
* Core class to access nav items via the REST API.
*
* @since 5.9.0
*
* @see WP_REST_Posts_Controller
*/
class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {
/**
* Gets the nav menu item, if the ID is valid.
*
* @since 5.9.0
*
* @param int $id Supplied ID.
* @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_nav_menu_item( $id ) {
$post = $this->get_post( $id );
if ( is_wp_error( $post ) ) {
return $post;
}
return wp_setup_nav_menu_item( $post );
}
/**
* Checks if a given request has access to read menu items.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$has_permission = parent::get_items_permissions_check( $request );
if ( true !== $has_permission ) {
return $has_permission;
}
return $this->check_has_read_only_access( $request );
}
/**
* Checks if a given request has access to read a menu item if they have access to edit them.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
*/
public function get_item_permissions_check( $request ) {
$permission_check = parent::get_item_permissions_check( $request );
if ( true !== $permission_check ) {
return $permission_check;
}
return $this->check_has_read_only_access( $request );
}
/**
* Checks whether the current user has read permission for the endpoint.
*
* This allows for any user that can `edit_theme_options` or edit any REST API available post type.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
protected function check_has_read_only_access( $request ) {
if ( current_user_can( 'edit_theme_options' ) ) {
return true;
}
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view menu items.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Creates a single post.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
}
$prepared_nav_item = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_nav_item ) ) {
return $prepared_nav_item;
}
$prepared_nav_item = (array) $prepared_nav_item;
$nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ), false );
if ( is_wp_error( $nav_menu_item_id ) ) {
if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) {
$nav_menu_item_id->add_data( array( 'status' => 500 ) );
} else {
$nav_menu_item_id->add_data( array( 'status' => 400 ) );
}
return $nav_menu_item_id;
}
$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
if ( is_wp_error( $nav_menu_item ) ) {
$nav_menu_item->add_data( array( 'status' => 404 ) );
return $nav_menu_item;
}
/**
* Fires after a single menu item is created or updated via the REST API.
*
* @since 5.9.0
*
* @param object $nav_menu_item Inserted or updated menu item object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a menu item, false when updating.
*/
do_action( 'rest_insert_nav_menu_item', $nav_menu_item, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
$fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a single menu item is completely created or updated via the REST API.
*
* @since 5.9.0
*
* @param object $nav_menu_item Inserted or updated menu item object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a menu item, false when updating.
*/
do_action( 'rest_after_insert_nav_menu_item', $nav_menu_item, $request, true );
$post = get_post( $nav_menu_item_id );
wp_after_insert_post( $post, false, null );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) );
return $response;
}
/**
* Updates a single nav menu item.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$valid_check = $this->get_nav_menu_item( $request['id'] );
if ( is_wp_error( $valid_check ) ) {
return $valid_check;
}
$post_before = get_post( $request['id'] );
$prepared_nav_item = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_nav_item ) ) {
return $prepared_nav_item;
}
$prepared_nav_item = (array) $prepared_nav_item;
$nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ), false );
if ( is_wp_error( $nav_menu_item_id ) ) {
if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) {
$nav_menu_item_id->add_data( array( 'status' => 500 ) );
} else {
$nav_menu_item_id->add_data( array( 'status' => 400 ) );
}
return $nav_menu_item_id;
}
$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
if ( is_wp_error( $nav_menu_item ) ) {
$nav_menu_item->add_data( array( 'status' => 404 ) );
return $nav_menu_item;
}
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
do_action( 'rest_insert_nav_menu_item', $nav_menu_item, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$post = get_post( $nav_menu_item_id );
$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
$fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
do_action( 'rest_after_insert_nav_menu_item', $nav_menu_item, $request, false );
wp_after_insert_post( $post, true, $post_before );
$response = $this->prepare_item_for_response( get_post( $nav_menu_item_id ), $request );
return rest_ensure_response( $response );
}
/**
* Deletes a single menu item.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error True on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$menu_item = $this->get_nav_menu_item( $request['id'] );
if ( is_wp_error( $menu_item ) ) {
return $menu_item;
}
// We don't support trashing for menu items.
if ( ! $request['force'] ) {
/* translators: %s: force=true */
return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
}
$previous = $this->prepare_item_for_response( get_post( $request['id'] ), $request );
$result = wp_delete_post( $request['id'], true );
if ( ! $result ) {
return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
}
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
/**
* Fires immediately after a single menu item is deleted via the REST API.
*
* @since 5.9.0
*
* @param object $nav_menu_item Inserted or updated menu item object.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request Request object.
*/
do_action( 'rest_delete_nav_menu_item', $menu_item, $response, $request );
return $response;
}
/**
* Prepares a single post for create or update.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Request object.
*
* @return object|WP_Error
*/
protected function prepare_item_for_database( $request ) {
$menu_item_db_id = $request['id'];
$menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id );
// Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138
if ( ! is_wp_error( $menu_item_obj ) ) {
// Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140
$position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;
$prepared_nav_item = array(
'menu-item-db-id' => $menu_item_db_id,
'menu-item-object-id' => $menu_item_obj->object_id,
'menu-item-object' => $menu_item_obj->object,
'menu-item-parent-id' => $menu_item_obj->menu_item_parent,
'menu-item-position' => $position,
'menu-item-type' => $menu_item_obj->type,
'menu-item-title' => $menu_item_obj->title,
'menu-item-url' => $menu_item_obj->url,
'menu-item-description' => $menu_item_obj->description,
'menu-item-attr-title' => $menu_item_obj->attr_title,
'menu-item-target' => $menu_item_obj->target,
'menu-item-classes' => $menu_item_obj->classes,
// Stored in the database as a string.
'menu-item-xfn' => explode( ' ', $menu_item_obj->xfn ),
'menu-item-status' => $menu_item_obj->post_status,
'menu-id' => $this->get_menu_id( $menu_item_db_id ),
);
} else {
$prepared_nav_item = array(
'menu-id' => 0,
'menu-item-db-id' => 0,
'menu-item-object-id' => 0,
'menu-item-object' => '',
'menu-item-parent-id' => 0,
'menu-item-position' => 1,
'menu-item-type' => 'custom',
'menu-item-title' => '',
'menu-item-url' => '',
'menu-item-description' => '',
'menu-item-attr-title' => '',
'menu-item-target' => '',
'menu-item-classes' => array(),
'menu-item-xfn' => array(),
'menu-item-status' => 'publish',
);
}
$mapping = array(
'menu-item-db-id' => 'id',
'menu-item-object-id' => 'object_id',
'menu-item-object' => 'object',
'menu-item-parent-id' => 'parent',
'menu-item-position' => 'menu_order',
'menu-item-type' => 'type',
'menu-item-url' => 'url',
'menu-item-description' => 'description',
'menu-item-attr-title' => 'attr_title',
'menu-item-target' => 'target',
'menu-item-classes' => 'classes',
'menu-item-xfn' => 'xfn',
'menu-item-status' => 'status',
);
$schema = $this->get_item_schema();
foreach ( $mapping as $original => $api_request ) {
if ( isset( $request[ $api_request ] ) ) {
$prepared_nav_item[ $original ] = $request[ $api_request ];
}
}
$taxonomy = get_taxonomy( 'nav_menu' );
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
// If menus submitted, cast to int.
if ( ! empty( $request[ $base ] ) ) {
$prepared_nav_item['menu-id'] = absint( $request[ $base ] );
}
// Nav menu title.
if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$prepared_nav_item['menu-item-title'] = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$prepared_nav_item['menu-item-title'] = $request['title']['raw'];
}
}
$error = new WP_Error();
// Check if object id exists before saving.
if ( ! $prepared_nav_item['menu-item-object'] ) {
// If taxonomy, check if term exists.
if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
$original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
if ( empty( $original ) || is_wp_error( $original ) ) {
$error->add( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) );
} else {
$prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
}
// If post, check if post object exists.
} elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
$original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
if ( empty( $original ) ) {
$error->add( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) );
} else {
$prepared_nav_item['menu-item-object'] = get_post_type( $original );
}
}
}
// If post type archive, check if post type exists.
if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
$post_type = $prepared_nav_item['menu-item-object'] ? $prepared_nav_item['menu-item-object'] : false;
$original = get_post_type_object( $post_type );
if ( ! $original ) {
$error->add( 'rest_post_invalid_type', __( 'Invalid post type.' ), array( 'status' => 400 ) );
}
}
// Check if menu item is type custom, then title and url are required.
if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
if ( '' === $prepared_nav_item['menu-item-title'] ) {
$error->add( 'rest_title_required', __( 'The title is required when using a custom menu item type.' ), array( 'status' => 400 ) );
}
if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
$error->add( 'rest_url_required', __( 'The url is required when using a custom menu item type.' ), array( 'status' => 400 ) );
}
}
if ( $error->has_errors() ) {
return $error;
}
// The xfn and classes properties are arrays, but passed to wp_update_nav_menu_item as a string.
foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
$prepared_nav_item[ $key ] = implode( ' ', $prepared_nav_item[ $key ] );
}
// Only draft / publish are valid post status for menu items.
if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
$prepared_nav_item['menu-item-status'] = 'draft';
}
$prepared_nav_item = (object) $prepared_nav_item;
/**
* Filters a menu item before it is inserted via the REST API.
*
* @since 5.9.0
*
* @param object $prepared_nav_item An object representing a single menu item prepared
* for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_pre_insert_nav_menu_item', $prepared_nav_item, $request );
}
/**
* Prepares a single post output for response.
*
* @since 5.9.0
*
* @param WP_Post $item Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Base fields for every post.
$fields = $this->get_fields_for_response( $request );
$menu_item = $this->get_nav_menu_item( $item->ID );
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $menu_item->ID;
}
if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $menu_item->title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
/** This filter is documented in wp-includes/post-template.php */
$title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID );
$data['title']['rendered'] = $title;
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}
if ( rest_is_field_included( 'status', $fields ) ) {
$data['status'] = $menu_item->post_status;
}
if ( rest_is_field_included( 'url', $fields ) ) {
$data['url'] = $menu_item->url;
}
if ( rest_is_field_included( 'attr_title', $fields ) ) {
// Same as post_excerpt.
$data['attr_title'] = $menu_item->attr_title;
}
if ( rest_is_field_included( 'description', $fields ) ) {
// Same as post_content.
$data['description'] = $menu_item->description;
}
if ( rest_is_field_included( 'type', $fields ) ) {
$data['type'] = $menu_item->type;
}
if ( rest_is_field_included( 'type_label', $fields ) ) {
$data['type_label'] = $menu_item->type_label;
}
if ( rest_is_field_included( 'object', $fields ) ) {
$data['object'] = $menu_item->object;
}
if ( rest_is_field_included( 'object_id', $fields ) ) {
// It is stored as a string, but should be exposed as an integer.
$data['object_id'] = absint( $menu_item->object_id );
}
if ( rest_is_field_included( 'parent', $fields ) ) {
// Same as post_parent, exposed as an integer.
$data['parent'] = (int) $menu_item->menu_item_parent;
}
if ( rest_is_field_included( 'menu_order', $fields ) ) {
// Same as post_parent, exposed as an integer.
$data['menu_order'] = (int) $menu_item->menu_order;
}
if ( rest_is_field_included( 'target', $fields ) ) {
$data['target'] = $menu_item->target;
}
if ( rest_is_field_included( 'classes', $fields ) ) {
$data['classes'] = (array) $menu_item->classes;
}
if ( rest_is_field_included( 'xfn', $fields ) ) {
$data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
}
if ( rest_is_field_included( 'invalid', $fields ) ) {
$data['invalid'] = (bool) $menu_item->_invalid;
}
if ( rest_is_field_included( 'meta', $fields ) ) {
$data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( rest_is_field_included( $base, $fields ) ) {
$terms = get_the_terms( $item, $taxonomy->name );
if ( ! is_array( $terms ) ) {
continue;
}
$term_ids = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
if ( 'nav_menu' === $taxonomy->name ) {
$data[ $base ] = $term_ids ? array_shift( $term_ids ) : 0;
} else {
$data[ $base ] = $term_ids;
}
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $item );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions( $item, $request );
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}
/**
* Filters the menu item data for a REST API response.
*
* @since 5.9.0
*
* @param WP_REST_Response $response The response object.
* @param object $menu_item Menu item setup by {@see wp_setup_nav_menu_item()}.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_nav_menu_item', $response, $menu_item, $request );
}
/**
* Prepares links for the request.
*
* @since 5.9.0
*
* @param WP_Post $post Post object.
* @return array Links for the given post.
*/
protected function prepare_links( $post ) {
$links = parent::prepare_links( $post );
$menu_item = $this->get_nav_menu_item( $post->ID );
if ( empty( $menu_item->object_id ) ) {
return $links;
}
$path = '';
$type = '';
$key = $menu_item->type;
if ( 'post_type' === $menu_item->type ) {
$path = rest_get_route_for_post( $menu_item->object_id );
$type = get_post_type( $menu_item->object_id );
} elseif ( 'taxonomy' === $menu_item->type ) {
$path = rest_get_route_for_term( $menu_item->object_id );
$type = get_term_field( 'taxonomy', $menu_item->object_id );
}
if ( $path && $type ) {
$links['https://api.w.org/menu-item-object'][] = array(
'href' => rest_url( $path ),
$key => $type,
'embeddable' => true,
);
}
return $links;
}
/**
* Retrieves Link Description Objects that should be added to the Schema for the posts collection.
*
* @since 5.9.0
*
* @return array
*/
protected function get_schema_links() {
$links = parent::get_schema_links();
$href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
$links[] = array(
'rel' => 'https://api.w.org/menu-item-object',
'title' => __( 'Get linked object.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'object' => array(
'type' => 'integer',
),
),
),
);
return $links;
}
/**
* Retrieves the term's schema, conforming to JSON Schema.
*
* @since 5.9.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
);
$schema['properties']['title'] = array(
'description' => __( 'The title for the object.' ),
'type' => array( 'string', 'object' ),
'context' => array( 'view', 'edit', 'embed' ),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the object, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML title for the object, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
$schema['properties']['id'] = array(
'description' => __( 'Unique identifier for the object.' ),
'type' => 'integer',
'default' => 0,
'minimum' => 0,
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['type_label'] = array(
'description' => __( 'The singular label used to describe this type of menu item.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
);
$schema['properties']['type'] = array(
'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
'type' => 'string',
'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
'context' => array( 'view', 'edit', 'embed' ),
'default' => 'custom',
);
$schema['properties']['status'] = array(
'description' => __( 'A named status for the object.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'default' => 'publish',
'context' => array( 'view', 'edit', 'embed' ),
);
$schema['properties']['parent'] = array(
'description' => __( 'The ID for the parent of the object.' ),
'type' => 'integer',
'minimum' => 0,
'default' => 0,
'context' => array( 'view', 'edit', 'embed' ),
);
$schema['properties']['attr_title'] = array(
'description' => __( 'Text for the title attribute of the link element for this menu item.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
);
$schema['properties']['classes'] = array(
'description' => __( 'Class names for the link element of this menu item.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => static function ( $value ) {
return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
},
),
);
$schema['properties']['description'] = array(
'description' => __( 'The description of this menu item.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
);
$schema['properties']['menu_order'] = array(
'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.' ),
'context' => array( 'view', 'edit', 'embed' ),
'type' => 'integer',
'minimum' => 1,
'default' => 1,
);
$schema['properties']['object'] = array(
'description' => __( 'The type of object originally represented, such as "category", "post", or "attachment".' ),
'context' => array( 'view', 'edit', 'embed' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_key',
),
);
$schema['properties']['object_id'] = array(
'description' => __( 'The database ID of the original object this menu item represents, for example the ID for posts or the term_id for categories.' ),
'context' => array( 'view', 'edit', 'embed' ),
'type' => 'integer',
'minimum' => 0,
'default' => 0,
);
$schema['properties']['target'] = array(
'description' => __( 'The target attribute of the link element for this menu item.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'enum' => array(
'_blank',
'',
),
);
$schema['properties']['url'] = array(
'description' => __( 'The URL to which this menu item points.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'validate_callback' => static function ( $url ) {
if ( '' === $url ) {
return true;
}
if ( sanitize_url( $url ) ) {
return true;
}
return new WP_Error(
'rest_invalid_url',
__( 'Invalid URL.' )
);
},
),
);
$schema['properties']['xfn'] = array(
'description' => __( 'The XFN relationship expressed in the link of this menu item.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => static function ( $value ) {
return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
},
),
);
$schema['properties']['invalid'] = array(
'description' => __( 'Whether the menu item represents an object that no longer exists.' ),
'context' => array( 'view', 'edit', 'embed' ),
'type' => 'boolean',
'readonly' => true,
);
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$schema['properties'][ $base ] = array(
/* translators: %s: taxonomy name */
'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
);
if ( 'nav_menu' === $taxonomy->name ) {
$schema['properties'][ $base ]['type'] = 'integer';
unset( $schema['properties'][ $base ]['items'] );
}
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
$schema_links = $this->get_schema_links();
if ( $schema_links ) {
$schema['links'] = $schema_links;
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for the posts collection.
*
* @since 5.9.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['menu_order'] = array(
'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'asc',
'enum' => array( 'asc', 'desc' ),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.' ),
'type' => 'string',
'default' => 'menu_order',
'enum' => array(
'author',
'date',
'id',
'include',
'modified',
'parent',
'relevance',
'slug',
'include_slugs',
'title',
'menu_order',
),
);
// Change default to 100 items.
$query_params['per_page']['default'] = 100;
return $query_params;
}
/**
* Determines the allowed query_vars for a get_items() response and prepares
* them for WP_Query.
*
* @since 5.9.0
*
* @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
* @param WP_REST_Request $request Optional. Full details about the request.
* @return array Items query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = parent::prepare_items_query( $prepared_args, $request );
// Map to proper WP_Query orderby param.
if ( isset( $query_args['orderby'], $request['orderby'] ) ) {
$orderby_mappings = array(
'id' => 'ID',
'include' => 'post__in',
'slug' => 'post_name',
'include_slugs' => 'post_name__in',
'menu_order' => 'menu_order',
);
if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
}
}
$query_args['update_menu_item_cache'] = true;
return $query_args;
}
/**
* Gets the id of the menu that the given menu item belongs to.
*
* @since 5.9.0
*
* @param int $menu_item_id Menu item id.
* @return int
*/
protected function get_menu_id( $menu_item_id ) {
$menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
$menu_id = 0;
if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
$menu_id = array_shift( $menu_ids );
}
return $menu_id;
}
}
endpoints/class-wp-rest-menu-locations-controller.php 0000644 00000020510 15153067731 0017106 0 ustar 00 <?php
/**
* REST API: WP_REST_Menu_Locations_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.9.0
*/
/**
* Core class used to access menu locations via the REST API.
*
* @since 5.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Menu_Locations_Controller extends WP_REST_Controller {
/**
* Menu Locations Constructor.
*
* @since 5.9.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'menu-locations';
}
/**
* Registers the routes for the objects of the controller.
*
* @since 5.9.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<location>[\w-]+)',
array(
'args' => array(
'location' => array(
'description' => __( 'An alphanumeric identifier for the menu location.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read menu locations.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view menu locations.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves all menu locations, depending on user context.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$data = array();
foreach ( get_registered_nav_menus() as $name => $description ) {
$location = new stdClass();
$location->name = $name;
$location->description = $description;
$location = $this->prepare_item_for_response( $location, $request );
$data[ $name ] = $this->prepare_response_for_collection( $location );
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read a menu location.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view menu locations.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves a specific menu location.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$registered_menus = get_registered_nav_menus();
if ( ! array_key_exists( $request['location'], $registered_menus ) ) {
return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
}
$location = new stdClass();
$location->name = $request['location'];
$location->description = $registered_menus[ $location->name ];
$data = $this->prepare_item_for_response( $location, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a menu location object for serialization.
*
* @since 5.9.0
*
* @param stdClass $item Post status data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Menu location data.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$location = $item;
$locations = get_nav_menu_locations();
$menu = isset( $locations[ $location->name ] ) ? $locations[ $location->name ] : 0;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'name', $fields ) ) {
$data['name'] = $location->name;
}
if ( rest_is_field_included( 'description', $fields ) ) {
$data['description'] = $location->description;
}
if ( rest_is_field_included( 'menu', $fields ) ) {
$data['menu'] = (int) $menu;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $location ) );
}
/**
* Filters menu location data returned from the REST API.
*
* @since 5.9.0
*
* @param WP_REST_Response $response The response object.
* @param object $location The original location object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_menu_location', $response, $location, $request );
}
/**
* Prepares links for the request.
*
* @since 5.9.0
*
* @param stdClass $location Menu location.
* @return array Links for the given menu location.
*/
protected function prepare_links( $location ) {
$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
// Entity meta.
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $location->name ),
),
'collection' => array(
'href' => rest_url( $base ),
),
);
$locations = get_nav_menu_locations();
$menu = isset( $locations[ $location->name ] ) ? $locations[ $location->name ] : 0;
if ( $menu ) {
$path = rest_get_route_for_term( $menu );
if ( $path ) {
$url = rest_url( $path );
$links['https://api.w.org/menu'][] = array(
'href' => $url,
'embeddable' => true,
);
}
}
return $links;
}
/**
* Retrieves the menu location's schema, conforming to JSON Schema.
*
* @since 5.9.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'menu-location',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The name of the menu location.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'The description of the menu location.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'menu' => array(
'description' => __( 'The ID of the assigned menu.' ),
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 5.9.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
endpoints/class-wp-rest-menus-controller.php 0000644 00000040676 15153067731 0015317 0 ustar 00 <?php
/**
* REST API: WP_REST_Menus_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.9.0
*/
/**
* Core class used to managed menu terms associated via the REST API.
*
* @since 5.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_Menus_Controller extends WP_REST_Terms_Controller {
/**
* Checks if a request has access to read menus.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
*/
public function get_items_permissions_check( $request ) {
$has_permission = parent::get_items_permissions_check( $request );
if ( true !== $has_permission ) {
return $has_permission;
}
return $this->check_has_read_only_access( $request );
}
/**
* Checks if a request has access to read or edit the specified menu.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
*/
public function get_item_permissions_check( $request ) {
$has_permission = parent::get_item_permissions_check( $request );
if ( true !== $has_permission ) {
return $has_permission;
}
return $this->check_has_read_only_access( $request );
}
/**
* Gets the term, if the ID is valid.
*
* @since 5.9.0
*
* @param int $id Supplied ID.
* @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
*/
protected function get_term( $id ) {
$term = parent::get_term( $id );
if ( is_wp_error( $term ) ) {
return $term;
}
$nav_term = wp_get_nav_menu_object( $term );
$nav_term->auto_add = $this->get_menu_auto_add( $nav_term->term_id );
return $nav_term;
}
/**
* Checks whether the current user has read permission for the endpoint.
*
* This allows for any user that can `edit_theme_options` or edit any REST API available post type.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the current user has permission, WP_Error object otherwise.
*/
protected function check_has_read_only_access( $request ) {
if ( current_user_can( 'edit_theme_options' ) ) {
return true;
}
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view menus.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Prepares a single term output for response.
*
* @since 5.9.0
*
* @param WP_Term $term Term object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $term, $request ) {
$nav_menu = wp_get_nav_menu_object( $term );
$response = parent::prepare_item_for_response( $nav_menu, $request );
$fields = $this->get_fields_for_response( $request );
$data = $response->get_data();
if ( rest_is_field_included( 'locations', $fields ) ) {
$data['locations'] = $this->get_menu_locations( $nav_menu->term_id );
}
if ( rest_is_field_included( 'auto_add', $fields ) ) {
$data['auto_add'] = $this->get_menu_auto_add( $nav_menu->term_id );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $term ) );
}
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $term, $request );
}
/**
* Prepares links for the request.
*
* @since 5.9.0
*
* @param WP_Term $term Term object.
* @return array Links for the given term.
*/
protected function prepare_links( $term ) {
$links = parent::prepare_links( $term );
$locations = $this->get_menu_locations( $term->term_id );
foreach ( $locations as $location ) {
$url = rest_url( sprintf( 'wp/v2/menu-locations/%s', $location ) );
$links['https://api.w.org/menu-location'][] = array(
'href' => $url,
'embeddable' => true,
);
}
return $links;
}
/**
* Prepares a single term for create or update.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Request object.
* @return object Prepared term data.
*/
public function prepare_item_for_database( $request ) {
$prepared_term = parent::prepare_item_for_database( $request );
$schema = $this->get_item_schema();
if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
$prepared_term->{'menu-name'} = $request['name'];
}
return $prepared_term;
}
/**
* Creates a single term in a taxonomy.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
}
$parent = wp_get_nav_menu_object( (int) $request['parent'] );
if ( ! $parent ) {
return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
}
}
$prepared_term = $this->prepare_item_for_database( $request );
$term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );
if ( is_wp_error( $term ) ) {
/*
* If we're going to inform the client that the term already exists,
* give them the identifier for future use.
*/
if ( in_array( 'menu_exists', $term->get_error_codes(), true ) ) {
$existing_term = get_term_by( 'name', $prepared_term->{'menu-name'}, $this->taxonomy );
$term->add_data( $existing_term->term_id, 'menu_exists' );
$term->add_data(
array(
'status' => 400,
'term_id' => $existing_term->term_id,
)
);
} else {
$term->add_data( array( 'status' => 400 ) );
}
return $term;
}
$term = $this->get_term( $term );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$locations_update = $this->handle_locations( $term->term_id, $request );
if ( is_wp_error( $locations_update ) ) {
return $locations_update;
}
$this->handle_auto_add( $term->term_id, $request );
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'view' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
$response = $this->prepare_item_for_response( $term, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
return $response;
}
/**
* Updates a single term from a taxonomy.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
}
$parent = get_term( (int) $request['parent'], $this->taxonomy );
if ( ! $parent ) {
return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
}
}
$prepared_term = $this->prepare_item_for_database( $request );
// Only update the term if we have something to update.
if ( ! empty( $prepared_term ) ) {
if ( ! isset( $prepared_term->{'menu-name'} ) ) {
// wp_update_nav_menu_object() requires that the menu-name is always passed.
$prepared_term->{'menu-name'} = $term->name;
}
$update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );
if ( is_wp_error( $update ) ) {
return $update;
}
}
$term = get_term( $term->term_id, $this->taxonomy );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$locations_update = $this->handle_locations( $term->term_id, $request );
if ( is_wp_error( $locations_update ) ) {
return $locations_update;
}
$this->handle_auto_add( $term->term_id, $request );
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'view' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Deletes a single term from a taxonomy.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
// We don't support trashing for terms.
if ( ! $request['force'] ) {
/* translators: %s: force=true */
return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menus do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
}
$request->set_param( 'context', 'view' );
$previous = $this->prepare_item_for_response( $term, $request );
$result = wp_delete_nav_menu( $term );
if ( ! $result || is_wp_error( $result ) ) {
return new WP_Error( 'rest_cannot_delete', __( 'The menu cannot be deleted.' ), array( 'status' => 500 ) );
}
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
return $response;
}
/**
* Returns the value of a menu's auto_add setting.
*
* @since 5.9.0
*
* @param int $menu_id The menu id to query.
* @return bool The value of auto_add.
*/
protected function get_menu_auto_add( $menu_id ) {
$nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
return in_array( $menu_id, $nav_menu_option['auto_add'], true );
}
/**
* Updates the menu's auto add from a REST request.
*
* @since 5.9.0
*
* @param int $menu_id The menu id to update.
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the auto add setting was successfully updated.
*/
protected function handle_auto_add( $menu_id, $request ) {
if ( ! isset( $request['auto_add'] ) ) {
return true;
}
$nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );
if ( ! isset( $nav_menu_option['auto_add'] ) ) {
$nav_menu_option['auto_add'] = array();
}
$auto_add = $request['auto_add'];
$i = array_search( $menu_id, $nav_menu_option['auto_add'], true );
if ( $auto_add && false === $i ) {
$nav_menu_option['auto_add'][] = $menu_id;
} elseif ( ! $auto_add && false !== $i ) {
array_splice( $nav_menu_option['auto_add'], $i, 1 );
}
$update = update_option( 'nav_menu_options', $nav_menu_option );
/** This action is documented in wp-includes/nav-menu.php */
do_action( 'wp_update_nav_menu', $menu_id );
return $update;
}
/**
* Returns the names of the locations assigned to the menu.
*
* @since 5.9.0
*
* @param int $menu_id The menu id.
* @return string[] The locations assigned to the menu.
*/
protected function get_menu_locations( $menu_id ) {
$locations = get_nav_menu_locations();
$menu_locations = array();
foreach ( $locations as $location => $assigned_menu_id ) {
if ( $menu_id === $assigned_menu_id ) {
$menu_locations[] = $location;
}
}
return $menu_locations;
}
/**
* Updates the menu's locations from a REST request.
*
* @since 5.9.0
*
* @param int $menu_id The menu id to update.
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True on success, a WP_Error on an error updating any of the locations.
*/
protected function handle_locations( $menu_id, $request ) {
if ( ! isset( $request['locations'] ) ) {
return true;
}
$menu_locations = get_registered_nav_menus();
$menu_locations = array_keys( $menu_locations );
$new_locations = array();
foreach ( $request['locations'] as $location ) {
if ( ! in_array( $location, $menu_locations, true ) ) {
return new WP_Error(
'rest_invalid_menu_location',
__( 'Invalid menu location.' ),
array(
'status' => 400,
'location' => $location,
)
);
}
$new_locations[ $location ] = $menu_id;
}
$assigned_menu = get_nav_menu_locations();
foreach ( $assigned_menu as $location => $term_id ) {
if ( $term_id === $menu_id ) {
unset( $assigned_menu[ $location ] );
}
}
$new_assignments = array_merge( $assigned_menu, $new_locations );
set_theme_mod( 'nav_menu_locations', $new_assignments );
return true;
}
/**
* Retrieves the term's schema, conforming to JSON Schema.
*
* @since 5.9.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = parent::get_item_schema();
unset( $schema['properties']['count'], $schema['properties']['link'], $schema['properties']['taxonomy'] );
$schema['properties']['locations'] = array(
'description' => __( 'The locations assigned to the menu.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'validate_callback' => static function ( $locations, $request, $param ) {
$valid = rest_validate_request_arg( $locations, $request, $param );
if ( true !== $valid ) {
return $valid;
}
$locations = rest_sanitize_request_arg( $locations, $request, $param );
foreach ( $locations as $location ) {
if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) {
return new WP_Error(
'rest_invalid_menu_location',
__( 'Invalid menu location.' ),
array(
'location' => $location,
)
);
}
}
return true;
},
),
);
$schema['properties']['auto_add'] = array(
'description' => __( 'Whether to automatically add top level pages to this menu.' ),
'context' => array( 'view', 'edit' ),
'type' => 'boolean',
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-navigation-fallback-controller.php 0000644 00000012063 15153067731 0020051 0 ustar 00 <?php
/**
* WP_REST_Navigation_Fallback_Controller class
*
* REST Controller to create/fetch a fallback Navigation Menu.
*
* @package WordPress
* @subpackage REST_API
* @since 6.3.0
*/
/**
* REST Controller to fetch a fallback Navigation Block Menu. If needed it creates one.
*
* @since 6.3.0
*/
class WP_REST_Navigation_Fallback_Controller extends WP_REST_Controller {
/**
* The Post Type for the Controller
*
* @since 6.3.0
*
* @var string
*/
private $post_type;
/**
* Constructs the controller.
*
* @since 6.3.0
*/
public function __construct() {
$this->namespace = 'wp-block-editor/v1';
$this->rest_base = 'navigation-fallback';
$this->post_type = 'wp_navigation';
}
/**
* Registers the controllers routes.
*
* @since 6.3.0
*/
public function register_routes() {
// Lists a single nav item based on the given id or slug.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read fallbacks.
*
* @since 6.3.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
// Getting fallbacks requires creating and reading `wp_navigation` posts.
if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( 'edit_theme_options' ) || ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create Navigation Menus as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit Navigation Menus as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Gets the most appropriate fallback Navigation Menu.
*
* @since 6.3.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$post = WP_Navigation_Fallback::get_fallback();
if ( empty( $post ) ) {
return rest_ensure_response( new WP_Error( 'no_fallback_menu', __( 'No fallback menu found.' ), array( 'status' => 404 ) ) );
}
$response = $this->prepare_item_for_response( $post, $request );
return $response;
}
/**
* Retrieves the fallbacks' schema, conforming to JSON Schema.
*
* @since 6.3.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'navigation-fallback',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'The unique identifier for the Navigation Menu.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Matches the post data to the schema we want.
*
* @since 6.3.0
*
* @param WP_Post $item The wp_navigation Post object whose response is being prepared.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response The response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = array();
$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = (int) $item->ID;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $item );
$response->add_links( $links );
}
return $response;
}
/**
* Prepares the links for the request.
*
* @since 6.3.0
*
* @param WP_Post $post the Navigation Menu post object.
* @return array Links for the given request.
*/
private function prepare_links( $post ) {
return array(
'self' => array(
'href' => rest_url( rest_get_route_for_post( $post->ID ) ),
'embeddable' => true,
),
);
}
}
endpoints/class-wp-rest-pattern-directory-controller.php 0000644 00000031420 15153067731 0017632 0 ustar 00 <?php
/**
* Block Pattern Directory REST API: WP_REST_Pattern_Directory_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Controller which provides REST endpoint for block patterns.
*
* This simply proxies the endpoint at http://api.wordpress.org/patterns/1.0/. That isn't necessary for
* functionality, but is desired for privacy. It prevents api.wordpress.org from knowing the user's IP address.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'pattern-directory';
}
/**
* Registers the necessary REST API routes.
*
* @since 5.8.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/patterns',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to view the local block pattern directory.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has permission, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_pattern_directory_cannot_view',
__( 'Sorry, you are not allowed to browse the local block pattern directory.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Search and retrieve block patterns metadata
*
* @since 5.8.0
* @since 6.0.0 Added 'slug' to request.
* @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
/*
* Include an unmodified `$wp_version`, so the API can craft a response that's tailored to
* it. Some plugins modify the version in a misguided attempt to improve security by
* obscuring the version, which can cause invalid requests.
*/
require ABSPATH . WPINC . '/version.php';
$valid_query_args = array(
'offset' => true,
'order' => true,
'orderby' => true,
'page' => true,
'per_page' => true,
'search' => true,
'slug' => true,
);
$query_args = array_intersect_key( $request->get_params(), $valid_query_args );
$query_args['locale'] = get_user_locale();
$query_args['wp-version'] = $wp_version;
$query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false;
$query_args['pattern-keywords'] = isset( $request['keyword'] ) ? $request['keyword'] : false;
$query_args = array_filter( $query_args );
$transient_key = $this->get_transient_key( $query_args );
/*
* Use network-wide transient to improve performance. The locale is the only site
* configuration that affects the response, and it's included in the transient key.
*/
$raw_patterns = get_site_transient( $transient_key );
if ( ! $raw_patterns ) {
$api_url = 'http://api.wordpress.org/patterns/1.0/?' . build_query( $query_args );
if ( wp_http_supports( array( 'ssl' ) ) ) {
$api_url = set_url_scheme( $api_url, 'https' );
}
/*
* Default to a short TTL, to mitigate cache stampedes on high-traffic sites.
* This assumes that most errors will be short-lived, e.g., packet loss that causes the
* first request to fail, but a follow-up one will succeed. The value should be high
* enough to avoid stampedes, but low enough to not interfere with users manually
* re-trying a failed request.
*/
$cache_ttl = 5;
$wporg_response = wp_remote_get( $api_url );
$raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) );
if ( is_wp_error( $wporg_response ) ) {
$raw_patterns = $wporg_response;
} elseif ( ! is_array( $raw_patterns ) ) {
// HTTP request succeeded, but response data is invalid.
$raw_patterns = new WP_Error(
'pattern_api_failed',
sprintf(
/* translators: %s: Support forums URL. */
__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
__( 'https://wordpress.org/support/forums/' )
),
array(
'response' => wp_remote_retrieve_body( $wporg_response ),
)
);
} else {
// Response has valid data.
$cache_ttl = HOUR_IN_SECONDS;
}
set_site_transient( $transient_key, $raw_patterns, $cache_ttl );
}
if ( is_wp_error( $raw_patterns ) ) {
$raw_patterns->add_data( array( 'status' => 500 ) );
return $raw_patterns;
}
$response = array();
if ( $raw_patterns ) {
foreach ( $raw_patterns as $pattern ) {
$response[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $pattern, $request )
);
}
}
return new WP_REST_Response( $response );
}
/**
* Prepare a raw block pattern before it gets output in a REST API response.
*
* @since 5.8.0
* @since 5.9.0 Renamed `$raw_pattern` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param object $item Raw pattern from api.wordpress.org, before any changes.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$raw_pattern = $item;
$prepared_pattern = array(
'id' => absint( $raw_pattern->id ),
'title' => sanitize_text_field( $raw_pattern->title->rendered ),
'content' => wp_kses_post( $raw_pattern->pattern_content ),
'categories' => array_map( 'sanitize_title', $raw_pattern->category_slugs ),
'keywords' => array_map( 'sanitize_text_field', explode( ',', $raw_pattern->meta->wpop_keywords ) ),
'description' => sanitize_text_field( $raw_pattern->meta->wpop_description ),
'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ),
'block_types' => array_map( 'sanitize_text_field', $raw_pattern->meta->wpop_block_types ),
);
$prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request );
$response = new WP_REST_Response( $prepared_pattern );
/**
* Filters the REST API response for a block pattern.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param object $raw_pattern The unprepared block pattern.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request );
}
/**
* Retrieves the block pattern's schema, conforming to JSON Schema.
*
* @since 5.8.0
* @since 6.2.0 Added `'block_types'` to schema.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'pattern-directory-item',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'The pattern ID.' ),
'type' => 'integer',
'minimum' => 1,
'context' => array( 'view', 'edit', 'embed' ),
),
'title' => array(
'description' => __( 'The pattern title, in human readable format.' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'edit', 'embed' ),
),
'content' => array(
'description' => __( 'The pattern content.' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'edit', 'embed' ),
),
'categories' => array(
'description' => __( "The pattern's category slugs." ),
'type' => 'array',
'uniqueItems' => true,
'items' => array( 'type' => 'string' ),
'context' => array( 'view', 'edit', 'embed' ),
),
'keywords' => array(
'description' => __( "The pattern's keywords." ),
'type' => 'array',
'uniqueItems' => true,
'items' => array( 'type' => 'string' ),
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => __( 'A description of the pattern.' ),
'type' => 'string',
'minLength' => 1,
'context' => array( 'view', 'edit', 'embed' ),
),
'viewport_width' => array(
'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'block_types' => array(
'description' => __( 'The block types which can use this pattern.' ),
'type' => 'array',
'uniqueItems' => true,
'items' => array( 'type' => 'string' ),
'context' => array( 'view', 'embed' ),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the search parameters for the block pattern's collection.
*
* @since 5.8.0
* @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['per_page']['default'] = 100;
$query_params['search']['minLength'] = 1;
$query_params['context']['default'] = 'view';
$query_params['category'] = array(
'description' => __( 'Limit results to those matching a category ID.' ),
'type' => 'integer',
'minimum' => 1,
);
$query_params['keyword'] = array(
'description' => __( 'Limit results to those matching a keyword ID.' ),
'type' => 'integer',
'minimum' => 1,
);
$query_params['slug'] = array(
'description' => __( 'Limit results to those matching a pattern (slug).' ),
'type' => 'array',
);
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by post attribute.' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'author',
'date',
'id',
'include',
'modified',
'parent',
'relevance',
'slug',
'include_slugs',
'title',
'favorite_count',
),
);
/**
* Filter collection parameters for the block pattern directory controller.
*
* @since 5.8.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_pattern_directory_collection_params', $query_params );
}
/*
* Include a hash of the query args, so that different requests are stored in
* separate caches.
*
* MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
* under the character limit for `_site_transient_timeout_{...}` keys.
*
* @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
*
* @since 6.0.0
*
* @param array $query_args Query arguments to generate a transient key from.
* @return string Transient key.
*/
protected function get_transient_key( $query_args ) {
if ( isset( $query_args['slug'] ) ) {
// This is an additional precaution because the "sort" function expects an array.
$query_args['slug'] = wp_parse_list( $query_args['slug'] );
// Empty arrays should not affect the transient key.
if ( empty( $query_args['slug'] ) ) {
unset( $query_args['slug'] );
} else {
// Sort the array so that the transient key doesn't depend on the order of slugs.
sort( $query_args['slug'] );
}
}
return 'wp_remote_block_patterns_' . md5( serialize( $query_args ) );
}
}
endpoints/class-wp-rest-plugins-controller.php 0000644 00000067561 15153067731 0015653 0 ustar 00 <?php
/**
* REST API: WP_REST_Plugins_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.5.0
*/
/**
* Core class to access plugins via the REST API.
*
* @since 5.5.0
*
* @see WP_REST_Controller
*/
class WP_REST_Plugins_Controller extends WP_REST_Controller {
const PATTERN = '[^.\/]+(?:\/[^.\/]+)?';
/**
* Plugins controller constructor.
*
* @since 5.5.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'plugins';
}
/**
* Registers the routes for the plugins controller.
*
* @since 5.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array(
'slug' => array(
'type' => 'string',
'required' => true,
'description' => __( 'WordPress.org plugin directory slug.' ),
'pattern' => '[\w\-]+',
),
'status' => array(
'description' => __( 'The plugin activation status.' ),
'type' => 'string',
'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
'default' => 'inactive',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<plugin>' . self::PATTERN . ')',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'plugin' => array(
'type' => 'string',
'pattern' => self::PATTERN,
'validate_callback' => array( $this, 'validate_plugin_param' ),
'sanitize_callback' => array( $this, 'sanitize_plugin_param' ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get plugins.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_cannot_view_plugins',
__( 'Sorry, you are not allowed to manage plugins for this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves a collection of plugins.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$plugins = array();
foreach ( get_plugins() as $file => $data ) {
if ( is_wp_error( $this->check_read_permission( $file ) ) ) {
continue;
}
$data['_file'] = $file;
if ( ! $this->does_plugin_match_request( $request, $data ) ) {
continue;
}
$plugins[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $data, $request ) );
}
return new WP_REST_Response( $plugins );
}
/**
* Checks if a given request has access to get a specific plugin.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_cannot_view_plugin',
__( 'Sorry, you are not allowed to manage plugins for this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$can_read = $this->check_read_permission( $request['plugin'] );
if ( is_wp_error( $can_read ) ) {
return $can_read;
}
return true;
}
/**
* Retrieves one plugin from the site.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$data = $this->get_plugin_data( $request['plugin'] );
if ( is_wp_error( $data ) ) {
return $data;
}
return $this->prepare_item_for_response( $data, $request );
}
/**
* Checks if the given plugin can be viewed by the current user.
*
* On multisite, this hides non-active network only plugins if the user does not have permission
* to manage network plugins.
*
* @since 5.5.0
*
* @param string $plugin The plugin file to check.
* @return true|WP_Error True if can read, a WP_Error instance otherwise.
*/
protected function check_read_permission( $plugin ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( ! $this->is_plugin_installed( $plugin ) ) {
return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
}
if ( ! is_multisite() ) {
return true;
}
if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) {
return true;
}
return new WP_Error(
'rest_cannot_view_plugin',
__( 'Sorry, you are not allowed to manage this plugin.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Checks if a given request has access to upload plugins.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) ) {
return new WP_Error(
'rest_cannot_install_plugin',
__( 'Sorry, you are not allowed to install plugins on this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_cannot_activate_plugin',
__( 'Sorry, you are not allowed to activate plugins.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Uploads a plugin and optionally activates it.
*
* @since 5.5.0
*
* @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
global $wp_filesystem;
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
$slug = $request['slug'];
// Verify filesystem is accessible first.
$filesystem_available = $this->is_filesystem_available();
if ( is_wp_error( $filesystem_available ) ) {
return $filesystem_available;
}
$api = plugins_api(
'plugin_information',
array(
'slug' => $slug,
'fields' => array(
'sections' => false,
'language_packs' => true,
),
)
);
if ( is_wp_error( $api ) ) {
if ( str_contains( $api->get_error_message(), 'Plugin not found.' ) ) {
$api->add_data( array( 'status' => 404 ) );
} else {
$api->add_data( array( 'status' => 500 ) );
}
return $api;
}
$skin = new WP_Ajax_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$result = $upgrader->install( $api->download_link );
if ( is_wp_error( $result ) ) {
$result->add_data( array( 'status' => 500 ) );
return $result;
}
// This should be the same as $result above.
if ( is_wp_error( $skin->result ) ) {
$skin->result->add_data( array( 'status' => 500 ) );
return $skin->result;
}
if ( $skin->get_errors()->has_errors() ) {
$error = $skin->get_errors();
$error->add_data( array( 'status' => 500 ) );
return $error;
}
if ( is_null( $result ) ) {
// Pass through the error from WP_Filesystem if one was raised.
if ( $wp_filesystem instanceof WP_Filesystem_Base
&& is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors()
) {
return new WP_Error(
'unable_to_connect_to_filesystem',
$wp_filesystem->errors->get_error_message(),
array( 'status' => 500 )
);
}
return new WP_Error(
'unable_to_connect_to_filesystem',
__( 'Unable to connect to the filesystem. Please confirm your credentials.' ),
array( 'status' => 500 )
);
}
$file = $upgrader->plugin_info();
if ( ! $file ) {
return new WP_Error(
'unable_to_determine_installed_plugin',
__( 'Unable to determine what plugin was installed.' ),
array( 'status' => 500 )
);
}
if ( 'inactive' !== $request['status'] ) {
$can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' );
if ( is_wp_error( $can_change_status ) ) {
return $can_change_status;
}
$changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' );
if ( is_wp_error( $changed_status ) ) {
return $changed_status;
}
}
// Install translations.
$installed_locales = array_values( get_available_languages() );
/** This filter is documented in wp-includes/update.php */
$installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales );
$language_packs = array_map(
static function ( $item ) {
return (object) $item;
},
$api->language_packs
);
$language_packs = array_filter(
$language_packs,
static function ( $pack ) use ( $installed_locales ) {
return in_array( $pack->language, $installed_locales, true );
}
);
if ( $language_packs ) {
$lp_upgrader = new Language_Pack_Upgrader( $skin );
// Install all applicable language packs for the plugin.
$lp_upgrader->bulk_upgrade( $language_packs );
}
$path = WP_PLUGIN_DIR . '/' . $file;
$data = get_plugin_data( $path, false, false );
$data['_file'] = $file;
$response = $this->prepare_item_for_response( $data, $request );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) );
return $response;
}
/**
* Checks if a given request has access to update a specific plugin.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_cannot_manage_plugins',
__( 'Sorry, you are not allowed to manage plugins for this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$can_read = $this->check_read_permission( $request['plugin'] );
if ( is_wp_error( $can_read ) ) {
return $can_read;
}
$status = $this->get_plugin_status( $request['plugin'] );
if ( $request['status'] && $status !== $request['status'] ) {
$can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status );
if ( is_wp_error( $can_change_status ) ) {
return $can_change_status;
}
}
return true;
}
/**
* Updates one plugin.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$data = $this->get_plugin_data( $request['plugin'] );
if ( is_wp_error( $data ) ) {
return $data;
}
$status = $this->get_plugin_status( $request['plugin'] );
if ( $request['status'] && $status !== $request['status'] ) {
$handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status );
if ( is_wp_error( $handled ) ) {
return $handled;
}
}
$this->update_additional_fields_for_object( $data, $request );
$request['context'] = 'edit';
return $this->prepare_item_for_response( $data, $request );
}
/**
* Checks if a given request has access to delete a specific plugin.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
if ( ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_cannot_manage_plugins',
__( 'Sorry, you are not allowed to manage plugins for this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! current_user_can( 'delete_plugins' ) ) {
return new WP_Error(
'rest_cannot_manage_plugins',
__( 'Sorry, you are not allowed to delete plugins for this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$can_read = $this->check_read_permission( $request['plugin'] );
if ( is_wp_error( $can_read ) ) {
return $can_read;
}
return true;
}
/**
* Deletes one plugin from the site.
*
* @since 5.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$data = $this->get_plugin_data( $request['plugin'] );
if ( is_wp_error( $data ) ) {
return $data;
}
if ( is_plugin_active( $request['plugin'] ) ) {
return new WP_Error(
'rest_cannot_delete_active_plugin',
__( 'Cannot delete an active plugin. Please deactivate it first.' ),
array( 'status' => 400 )
);
}
$filesystem_available = $this->is_filesystem_available();
if ( is_wp_error( $filesystem_available ) ) {
return $filesystem_available;
}
$prepared = $this->prepare_item_for_response( $data, $request );
$deleted = delete_plugins( array( $request['plugin'] ) );
if ( is_wp_error( $deleted ) ) {
$deleted->add_data( array( 'status' => 500 ) );
return $deleted;
}
return new WP_REST_Response(
array(
'deleted' => true,
'previous' => $prepared->get_data(),
)
);
}
/**
* Prepares the plugin for the REST response.
*
* @since 5.5.0
*
* @param array $item Unmarked up and untranslated plugin data from {@see get_plugin_data()}.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$item = _get_plugin_data_markup_translate( $item['_file'], $item, false );
$marked = _get_plugin_data_markup_translate( $item['_file'], $item, true );
$data = array(
'plugin' => substr( $item['_file'], 0, - 4 ),
'status' => $this->get_plugin_status( $item['_file'] ),
'name' => $item['Name'],
'plugin_uri' => $item['PluginURI'],
'author' => $item['Author'],
'author_uri' => $item['AuthorURI'],
'description' => array(
'raw' => $item['Description'],
'rendered' => $marked['Description'],
),
'version' => $item['Version'],
'network_only' => $item['Network'],
'requires_wp' => $item['RequiresWP'],
'requires_php' => $item['RequiresPHP'],
'textdomain' => $item['TextDomain'],
);
$data = $this->add_additional_fields_to_object( $data, $request );
$response = new WP_REST_Response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $item ) );
}
/**
* Filters plugin data for a REST API response.
*
* @since 5.5.0
*
* @param WP_REST_Response $response The response object.
* @param array $item The plugin item from {@see get_plugin_data()}.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_plugin', $response, $item, $request );
}
/**
* Prepares links for the request.
*
* @since 5.5.0
*
* @param array $item The plugin item.
* @return array[]
*/
protected function prepare_links( $item ) {
return array(
'self' => array(
'href' => rest_url(
sprintf(
'%s/%s/%s',
$this->namespace,
$this->rest_base,
substr( $item['_file'], 0, - 4 )
)
),
),
);
}
/**
* Gets the plugin header data for a plugin.
*
* @since 5.5.0
*
* @param string $plugin The plugin file to get data for.
* @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed.
*/
protected function get_plugin_data( $plugin ) {
$plugins = get_plugins();
if ( ! isset( $plugins[ $plugin ] ) ) {
return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
}
$data = $plugins[ $plugin ];
$data['_file'] = $plugin;
return $data;
}
/**
* Get's the activation status for a plugin.
*
* @since 5.5.0
*
* @param string $plugin The plugin file to check.
* @return string Either 'network-active', 'active' or 'inactive'.
*/
protected function get_plugin_status( $plugin ) {
if ( is_plugin_active_for_network( $plugin ) ) {
return 'network-active';
}
if ( is_plugin_active( $plugin ) ) {
return 'active';
}
return 'inactive';
}
/**
* Handle updating a plugin's status.
*
* @since 5.5.0
*
* @param string $plugin The plugin file to update.
* @param string $new_status The plugin's new status.
* @param string $current_status The plugin's current status.
* @return true|WP_Error
*/
protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) {
if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) {
return new WP_Error(
'rest_cannot_manage_network_plugins',
__( 'Sorry, you are not allowed to manage network plugins.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) {
return new WP_Error(
'rest_cannot_activate_plugin',
__( 'Sorry, you are not allowed to activate this plugin.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) {
return new WP_Error(
'rest_cannot_deactivate_plugin',
__( 'Sorry, you are not allowed to deactivate this plugin.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Handle updating a plugin's status.
*
* @since 5.5.0
*
* @param string $plugin The plugin file to update.
* @param string $new_status The plugin's new status.
* @param string $current_status The plugin's current status.
* @return true|WP_Error
*/
protected function handle_plugin_status( $plugin, $new_status, $current_status ) {
if ( 'inactive' === $new_status ) {
deactivate_plugins( $plugin, false, 'network-active' === $current_status );
return true;
}
if ( 'active' === $new_status && 'network-active' === $current_status ) {
return true;
}
$network_activate = 'network-active' === $new_status;
if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) {
return new WP_Error(
'rest_network_only_plugin',
__( 'Network only plugin must be network activated.' ),
array( 'status' => 400 )
);
}
$activated = activate_plugin( $plugin, '', $network_activate );
if ( is_wp_error( $activated ) ) {
$activated->add_data( array( 'status' => 500 ) );
return $activated;
}
return true;
}
/**
* Checks that the "plugin" parameter is a valid path.
*
* @since 5.5.0
*
* @param string $file The plugin file parameter.
* @return bool
*/
public function validate_plugin_param( $file ) {
if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) {
return false;
}
$validated = validate_file( plugin_basename( $file ) );
return 0 === $validated;
}
/**
* Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended.
*
* @since 5.5.0
*
* @param string $file The plugin file parameter.
* @return string
*/
public function sanitize_plugin_param( $file ) {
return plugin_basename( sanitize_text_field( $file . '.php' ) );
}
/**
* Checks if the plugin matches the requested parameters.
*
* @since 5.5.0
*
* @param WP_REST_Request $request The request to require the plugin matches against.
* @param array $item The plugin item.
* @return bool
*/
protected function does_plugin_match_request( $request, $item ) {
$search = $request['search'];
if ( $search ) {
$matched_search = false;
foreach ( $item as $field ) {
if ( is_string( $field ) && str_contains( strip_tags( $field ), $search ) ) {
$matched_search = true;
break;
}
}
if ( ! $matched_search ) {
return false;
}
}
$status = $request['status'];
if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) {
return false;
}
return true;
}
/**
* Checks if the plugin is installed.
*
* @since 5.5.0
*
* @param string $plugin The plugin file.
* @return bool
*/
protected function is_plugin_installed( $plugin ) {
return file_exists( WP_PLUGIN_DIR . '/' . $plugin );
}
/**
* Determine if the endpoints are available.
*
* Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present.
*
* @since 5.5.0
*
* @return true|WP_Error True if filesystem is available, WP_Error otherwise.
*/
protected function is_filesystem_available() {
$filesystem_method = get_filesystem_method();
if ( 'direct' === $filesystem_method ) {
return true;
}
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
if ( $filesystem_credentials_are_stored ) {
return true;
}
return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.' ), array( 'status' => 500 ) );
}
/**
* Retrieves the plugin's schema, conforming to JSON Schema.
*
* @since 5.5.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'plugin',
'type' => 'object',
'properties' => array(
'plugin' => array(
'description' => __( 'The plugin file.' ),
'type' => 'string',
'pattern' => self::PATTERN,
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'status' => array(
'description' => __( 'The plugin activation status.' ),
'type' => 'string',
'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
'context' => array( 'view', 'edit', 'embed' ),
),
'name' => array(
'description' => __( 'The plugin name.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'plugin_uri' => array(
'description' => __( 'The plugin\'s website address.' ),
'type' => 'string',
'format' => 'uri',
'readonly' => true,
'context' => array( 'view', 'edit' ),
),
'author' => array(
'description' => __( 'The plugin author.' ),
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
),
'author_uri' => array(
'description' => __( 'Plugin author\'s website address.' ),
'type' => 'string',
'format' => 'uri',
'readonly' => true,
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'The plugin description.' ),
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'properties' => array(
'raw' => array(
'description' => __( 'The raw plugin description.' ),
'type' => 'string',
),
'rendered' => array(
'description' => __( 'The plugin description formatted for display.' ),
'type' => 'string',
),
),
),
'version' => array(
'description' => __( 'The plugin version number.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
),
'network_only' => array(
'description' => __( 'Whether the plugin can only be activated network-wide.' ),
'type' => 'boolean',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'requires_wp' => array(
'description' => __( 'Minimum required version of WordPress.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'requires_php' => array(
'description' => __( 'Minimum required version of PHP.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'textdomain' => array(
'description' => __( 'The plugin\'s text domain.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for the collections.
*
* @since 5.5.0
*
* @return array Query parameters for the collection.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['status'] = array(
'description' => __( 'Limits results to plugins with the given status.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
),
);
unset( $query_params['page'], $query_params['per_page'] );
return $query_params;
}
}
endpoints/class-wp-rest-post-statuses-controller.php 0000644 00000024116 15153067731 0017015 0 ustar 00 <?php
/**
* REST API: WP_REST_Post_Statuses_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to access post statuses via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'statuses';
}
/**
* Registers the routes for post statuses.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<status>[\w-]+)',
array(
'args' => array(
'status' => array(
'description' => __( 'An alphanumeric identifier for the status.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read post statuses.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to manage post statuses.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves all post statuses, depending on user context.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$data = array();
$statuses = get_post_stati( array( 'internal' => false ), 'object' );
$statuses['trash'] = get_post_status_object( 'trash' );
foreach ( $statuses as $slug => $obj ) {
$ret = $this->check_read_permission( $obj );
if ( ! $ret ) {
continue;
}
$status = $this->prepare_item_for_response( $obj, $request );
$data[ $obj->name ] = $this->prepare_response_for_collection( $status );
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read a post status.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$status = get_post_status_object( $request['status'] );
if ( empty( $status ) ) {
return new WP_Error(
'rest_status_invalid',
__( 'Invalid status.' ),
array( 'status' => 404 )
);
}
$check = $this->check_read_permission( $status );
if ( ! $check ) {
return new WP_Error(
'rest_cannot_read_status',
__( 'Cannot view status.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Checks whether a given post status should be visible.
*
* @since 4.7.0
*
* @param object $status Post status.
* @return bool True if the post status is visible, otherwise false.
*/
protected function check_read_permission( $status ) {
if ( true === $status->public ) {
return true;
}
if ( false === $status->internal || 'trash' === $status->name ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
}
return false;
}
/**
* Retrieves a specific post status.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$obj = get_post_status_object( $request['status'] );
if ( empty( $obj ) ) {
return new WP_Error(
'rest_status_invalid',
__( 'Invalid status.' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a post status object for serialization.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$status` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param stdClass $item Post status data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Post status data.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$status = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'name', $fields, true ) ) {
$data['name'] = $status->label;
}
if ( in_array( 'private', $fields, true ) ) {
$data['private'] = (bool) $status->private;
}
if ( in_array( 'protected', $fields, true ) ) {
$data['protected'] = (bool) $status->protected;
}
if ( in_array( 'public', $fields, true ) ) {
$data['public'] = (bool) $status->public;
}
if ( in_array( 'queryable', $fields, true ) ) {
$data['queryable'] = (bool) $status->publicly_queryable;
}
if ( in_array( 'show_in_list', $fields, true ) ) {
$data['show_in_list'] = (bool) $status->show_in_admin_all_list;
}
if ( in_array( 'slug', $fields, true ) ) {
$data['slug'] = $status->name;
}
if ( in_array( 'date_floating', $fields, true ) ) {
$data['date_floating'] = $status->date_floating;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$rest_url = rest_url( rest_get_route_for_post_type_items( 'post' ) );
if ( 'publish' === $status->name ) {
$response->add_link( 'archives', $rest_url );
} else {
$response->add_link( 'archives', add_query_arg( 'status', $status->name, $rest_url ) );
}
/**
* Filters a post status returned from the REST API.
*
* Allows modification of the status data right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param object $status The original post status object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_status', $response, $status, $request );
}
/**
* Retrieves the post status' schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'status',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The title for the status.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'private' => array(
'description' => __( 'Whether posts with this status should be private.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether posts with this status should be protected.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'public' => array(
'description' => __( 'Whether posts of this status should be shown in the front end of the site.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'queryable' => array(
'description' => __( 'Whether posts with this status should be publicly-queryable.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'show_in_list' => array(
'description' => __( 'Whether to include posts in the edit listing for their post type.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the status.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'date_floating' => array(
'description' => __( 'Whether posts of this status may have floating published dates.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
endpoints/class-wp-rest-post-types-controller.php 0000644 00000031205 15153067731 0016303 0 ustar 00 <?php
/**
* REST API: WP_REST_Post_Types_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to access post types via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Post_Types_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'types';
}
/**
* Registers the routes for post types.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<type>[\w-]+)',
array(
'args' => array(
'type' => array(
'description' => __( 'An alphanumeric identifier for the post type.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => '__return_true',
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read types.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to edit posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves all public post types.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$data = array();
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( 'edit' === $request['context'] && ! current_user_can( $type->cap->edit_posts ) ) {
continue;
}
$post_type = $this->prepare_item_for_response( $type, $request );
$data[ $type->name ] = $this->prepare_response_for_collection( $post_type );
}
return rest_ensure_response( $data );
}
/**
* Retrieves a specific post type.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$obj = get_post_type_object( $request['type'] );
if ( empty( $obj ) ) {
return new WP_Error(
'rest_type_invalid',
__( 'Invalid post type.' ),
array( 'status' => 404 )
);
}
if ( empty( $obj->show_in_rest ) ) {
return new WP_Error(
'rest_cannot_read_type',
__( 'Cannot view post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$data = $this->prepare_item_for_response( $obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a post type object for serialization.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$post_type` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Post_Type $item Post type object.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post_type = $item;
$taxonomies = wp_list_filter( get_object_taxonomies( $post_type->name, 'objects' ), array( 'show_in_rest' => true ) );
$taxonomies = wp_list_pluck( $taxonomies, 'name' );
$base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
$namespace = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2';
$supports = get_all_post_type_supports( $post_type->name );
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'capabilities', $fields ) ) {
$data['capabilities'] = $post_type->cap;
}
if ( rest_is_field_included( 'description', $fields ) ) {
$data['description'] = $post_type->description;
}
if ( rest_is_field_included( 'hierarchical', $fields ) ) {
$data['hierarchical'] = $post_type->hierarchical;
}
if ( rest_is_field_included( 'has_archive', $fields ) ) {
$data['has_archive'] = $post_type->has_archive;
}
if ( rest_is_field_included( 'visibility', $fields ) ) {
$data['visibility'] = array(
'show_in_nav_menus' => (bool) $post_type->show_in_nav_menus,
'show_ui' => (bool) $post_type->show_ui,
);
}
if ( rest_is_field_included( 'viewable', $fields ) ) {
$data['viewable'] = is_post_type_viewable( $post_type );
}
if ( rest_is_field_included( 'labels', $fields ) ) {
$data['labels'] = $post_type->labels;
}
if ( rest_is_field_included( 'name', $fields ) ) {
$data['name'] = $post_type->label;
}
if ( rest_is_field_included( 'slug', $fields ) ) {
$data['slug'] = $post_type->name;
}
if ( rest_is_field_included( 'icon', $fields ) ) {
$data['icon'] = $post_type->menu_icon;
}
if ( rest_is_field_included( 'supports', $fields ) ) {
$data['supports'] = $supports;
}
if ( rest_is_field_included( 'taxonomies', $fields ) ) {
$data['taxonomies'] = array_values( $taxonomies );
}
if ( rest_is_field_included( 'rest_base', $fields ) ) {
$data['rest_base'] = $base;
}
if ( rest_is_field_included( 'rest_namespace', $fields ) ) {
$data['rest_namespace'] = $namespace;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $post_type ) );
}
/**
* Filters a post type returned from the REST API.
*
* Allows modification of the post type data right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post_Type $post_type The original post type object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
}
/**
* Prepares links for the request.
*
* @since 6.1.0
*
* @param WP_Post_Type $post_type The post type.
* @return array Links for the given post type.
*/
protected function prepare_links( $post_type ) {
return array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'https://api.w.org/items' => array(
'href' => rest_url( rest_get_route_for_post_type_items( $post_type->name ) ),
),
);
}
/**
* Retrieves the post type's schema, conforming to JSON Schema.
*
* @since 4.7.0
* @since 4.8.0 The `supports` property was added.
* @since 5.9.0 The `visibility` and `rest_namespace` properties were added.
* @since 6.1.0 The `icon` property was added.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'type',
'type' => 'object',
'properties' => array(
'capabilities' => array(
'description' => __( 'All capabilities used by the post type.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the post type.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hierarchical' => array(
'description' => __( 'Whether or not the post type should have children.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'viewable' => array(
'description' => __( 'Whether or not the post type can be viewed.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'labels' => array(
'description' => __( 'Human-readable labels for the post type for various contexts.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The title for the post type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the post type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'supports' => array(
'description' => __( 'All features, supported by the post type.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'has_archive' => array(
'description' => __( 'If the value is a string, the value will be used as the archive slug. If the value is false the post type has no archive.' ),
'type' => array( 'string', 'boolean' ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxonomies' => array(
'description' => __( 'Taxonomies associated with post type.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rest_base' => array(
'description' => __( 'REST base route for the post type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'rest_namespace' => array(
'description' => __( 'REST route\'s namespace for the post type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'visibility' => array(
'description' => __( 'The visibility settings for the post type.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
'properties' => array(
'show_ui' => array(
'description' => __( 'Whether to generate a default UI for managing this post type.' ),
'type' => 'boolean',
),
'show_in_nav_menus' => array(
'description' => __( 'Whether to make the post type available for selection in navigation menus.' ),
'type' => 'boolean',
),
),
),
'icon' => array(
'description' => __( 'The icon for the post type.' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
endpoints/class-wp-rest-posts-controller.php 0000644 00000275550 15153067731 0015341 0 ustar 00 <?php
/**
* REST API: WP_REST_Posts_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to access posts via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Posts_Controller extends WP_REST_Controller {
/**
* Post type.
*
* @since 4.7.0
* @var string
*/
protected $post_type;
/**
* Instance of a post meta fields object.
*
* @since 4.7.0
* @var WP_REST_Post_Meta_Fields
*/
protected $meta;
/**
* Passwordless post access permitted.
*
* @since 5.7.1
* @var int[]
*/
protected $password_check_passed = array();
/**
* Whether the controller supports batching.
*
* @since 5.9.0
* @var array
*/
protected $allow_batch = array( 'v1' => true );
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $post_type Post type.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
}
/**
* Registers the routes for posts.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
$schema = $this->get_item_schema();
$get_item_args = array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
if ( isset( $schema['properties']['password'] ) ) {
$get_item_args['password'] = array(
'description' => __( 'The password for the post if it is password protected.' ),
'type' => 'string',
);
}
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $get_item_args,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass Trash and force deletion.' ),
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$post_type = get_post_type_object( $this->post_type );
if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Overrides the result of the post password check for REST requested posts.
*
* Allow users to read the content of password protected posts if they have
* previously passed a permission check or if they have the `edit_post` capability
* for the post being checked.
*
* @since 5.7.1
*
* @param bool $required Whether the post requires a password check.
* @param WP_Post $post The post been password checked.
* @return bool Result of password check taking in to account REST API considerations.
*/
public function check_password_required( $required, $post ) {
if ( ! $required ) {
return $required;
}
$post = get_post( $post );
if ( ! $post ) {
return $required;
}
if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) {
// Password previously checked and approved.
return false;
}
return ! current_user_can( 'edit_post', $post->ID );
}
/**
* Retrieves a collection of posts.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Ensure a search string is set in case the orderby is set to 'relevance'.
if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
return new WP_Error(
'rest_no_search_term_defined',
__( 'You need to define a search term to order by relevance.' ),
array( 'status' => 400 )
);
}
// Ensure an include parameter is set in case the orderby is set to 'include'.
if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
return new WP_Error(
'rest_orderby_include_missing_include',
__( 'You need to define an include parameter to order by include.' ),
array( 'status' => 400 )
);
}
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
$args = array();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'author' => 'author__in',
'author_exclude' => 'author__not_in',
'exclude' => 'post__not_in',
'include' => 'post__in',
'menu_order' => 'menu_order',
'offset' => 'offset',
'order' => 'order',
'orderby' => 'orderby',
'page' => 'paged',
'parent' => 'post_parent__in',
'parent_exclude' => 'post_parent__not_in',
'search' => 's',
'search_columns' => 'search_columns',
'slug' => 'post_name__in',
'status' => 'post_status',
);
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$args[ $wp_param ] = $request[ $api_param ];
}
}
// Check for & assign any parameters which require special handling or setting.
$args['date_query'] = array();
if ( isset( $registered['before'], $request['before'] ) ) {
$args['date_query'][] = array(
'before' => $request['before'],
'column' => 'post_date',
);
}
if ( isset( $registered['modified_before'], $request['modified_before'] ) ) {
$args['date_query'][] = array(
'before' => $request['modified_before'],
'column' => 'post_modified',
);
}
if ( isset( $registered['after'], $request['after'] ) ) {
$args['date_query'][] = array(
'after' => $request['after'],
'column' => 'post_date',
);
}
if ( isset( $registered['modified_after'], $request['modified_after'] ) ) {
$args['date_query'][] = array(
'after' => $request['modified_after'],
'column' => 'post_modified',
);
}
// Ensure our per_page parameter overrides any provided posts_per_page filter.
if ( isset( $registered['per_page'] ) ) {
$args['posts_per_page'] = $request['per_page'];
}
if ( isset( $registered['sticky'], $request['sticky'] ) ) {
$sticky_posts = get_option( 'sticky_posts', array() );
if ( ! is_array( $sticky_posts ) ) {
$sticky_posts = array();
}
if ( $request['sticky'] ) {
/*
* As post__in will be used to only get sticky posts,
* we have to support the case where post__in was already
* specified.
*/
$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;
/*
* If we intersected, but there are no post IDs in common,
* WP_Query won't return "no posts" for post__in = array()
* so we have to fake it a bit.
*/
if ( ! $args['post__in'] ) {
$args['post__in'] = array( 0 );
}
} elseif ( $sticky_posts ) {
/*
* As post___not_in will be used to only get posts that
* are not sticky, we have to support the case where post__not_in
* was already specified.
*/
$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
}
}
$args = $this->prepare_tax_query( $args, $request );
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
/**
* Filters WP_Query arguments when querying posts via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_post_query`
* - `rest_page_query`
* - `rest_attachment_query`
*
* Enables adding extra arguments or setting defaults for a post collection request.
*
* @since 4.7.0
* @since 5.7.0 Moved after the `tax_query` query arg is generated.
*
* @link https://developer.wordpress.org/reference/classes/wp_query/
*
* @param array $args Array of arguments for WP_Query.
* @param WP_REST_Request $request The REST API request.
*/
$args = apply_filters( "rest_{$this->post_type}_query", $args, $request );
$query_args = $this->prepare_items_query( $args, $request );
$posts_query = new WP_Query();
$query_result = $posts_query->query( $query_args );
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
$posts = array();
update_post_author_caches( $query_result );
update_post_parent_caches( $query_result );
if ( post_type_supports( $this->post_type, 'thumbnail' ) ) {
update_post_thumbnail_cache( $posts_query );
}
foreach ( $query_result as $post ) {
if ( ! $this->check_read_permission( $post ) ) {
continue;
}
$data = $this->prepare_item_for_response( $post, $request );
$posts[] = $this->prepare_response_for_collection( $data );
}
// Reset filter.
if ( 'edit' === $request['context'] ) {
remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
$page = (int) $query_args['paged'];
$total_posts = $posts_query->found_posts;
if ( $total_posts < 1 && $page > 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
$max_pages = (int) ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );
if ( $page > $max_pages && $total_posts > 0 ) {
return new WP_Error(
'rest_post_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
$response = rest_ensure_response( $posts );
$response->header( 'X-WP-Total', (int) $total_posts );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$request_params = $request->get_query_params();
$collection_url = rest_url( rest_get_route_for_post_type_items( $this->post_type ) );
$base = add_query_arg( urlencode_deep( $request_params ), $collection_url );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Gets the post, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_post( $id ) {
$error = new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$post = get_post( (int) $id );
if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
return $error;
}
return $post;
}
/**
* Checks if a given request has access to read a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
*/
public function get_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( $post && ! empty( $request['password'] ) ) {
// Check post password, and return error if invalid.
if ( ! hash_equals( $post->post_password, $request['password'] ) ) {
return new WP_Error(
'rest_post_incorrect_password',
__( 'Incorrect post password.' ),
array( 'status' => 403 )
);
}
}
// Allow access to all password protected posts if the context is edit.
if ( 'edit' === $request['context'] ) {
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
}
if ( $post ) {
return $this->check_read_permission( $post );
}
return true;
}
/**
* Checks if the user can access password-protected content.
*
* This method determines whether we need to override the regular password
* check in core with a filter.
*
* @since 4.7.0
*
* @param WP_Post $post Post to check against.
* @param WP_REST_Request $request Request data to check.
* @return bool True if the user can access password-protected content, otherwise false.
*/
public function can_access_password_content( $post, $request ) {
if ( empty( $post->post_password ) ) {
// No filter required.
return false;
}
/*
* Users always gets access to password protected content in the edit
* context if they have the `edit_post` meta capability.
*/
if (
'edit' === $request['context'] &&
current_user_can( 'edit_post', $post->ID )
) {
return true;
}
// No password, no auth.
if ( empty( $request['password'] ) ) {
return false;
}
// Double-check the request password.
return hash_equals( $post->post_password, $request['password'] );
}
/**
* Retrieves a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$data = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $data );
if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
}
return $response;
}
/**
* Checks if a given request has access to create a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_post_exists',
__( 'Cannot create existing post.' ),
array( 'status' => 400 )
);
}
$post_type = get_post_type_object( $this->post_type );
if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
return new WP_Error(
'rest_cannot_edit_others',
__( 'Sorry, you are not allowed to create posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_assign_sticky',
__( 'Sorry, you are not allowed to make posts sticky.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! current_user_can( $post_type->cap->create_posts ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! $this->check_assign_terms_permission( $request ) ) {
return new WP_Error(
'rest_cannot_assign_term',
__( 'Sorry, you are not allowed to assign the provided terms.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_post_exists',
__( 'Cannot create existing post.' ),
array( 'status' => 400 )
);
}
$prepared_post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_post ) ) {
return $prepared_post;
}
$prepared_post->post_type = $this->post_type;
if ( ! empty( $prepared_post->post_name )
&& ! empty( $prepared_post->post_status )
&& in_array( $prepared_post->post_status, array( 'draft', 'pending' ), true )
) {
/*
* `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts.
*
* To ensure that a unique slug is generated, pass the post data with the 'publish' status.
*/
$prepared_post->post_name = wp_unique_post_slug(
$prepared_post->post_name,
$prepared_post->id,
'publish',
$prepared_post->post_type,
$prepared_post->post_parent
);
}
$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false );
if ( is_wp_error( $post_id ) ) {
if ( 'db_insert_error' === $post_id->get_error_code() ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post = get_post( $post_id );
/**
* Fires after a single post is created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_insert_post`
* - `rest_insert_page`
* - `rest_insert_attachment`
*
* @since 4.7.0
*
* @param WP_Post $post Inserted or updated post object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "rest_insert_{$this->post_type}", $post, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['sticky'] ) ) {
if ( ! empty( $request['sticky'] ) ) {
stick_post( $post_id );
} else {
unstick_post( $post_id );
}
}
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$this->handle_featured_media( $request['featured_media'], $post_id );
}
if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
set_post_format( $post, $request['format'] );
}
if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
$this->handle_template( $request['template'], $post_id, true );
}
$terms_update = $this->handle_terms( $post_id, $request );
if ( is_wp_error( $terms_update ) ) {
return $terms_update;
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $post_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$post = get_post( $post_id );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a single post is completely created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_after_insert_post`
* - `rest_after_insert_page`
* - `rest_after_insert_attachment`
*
* @since 5.0.0
*
* @param WP_Post $post Inserted or updated post object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a post, false when updating.
*/
do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
wp_after_insert_post( $post, false, null );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( rest_get_route_for_post( $post ) ) );
return $response;
}
/**
* Checks if a given request has access to update a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$post_type = get_post_type_object( $this->post_type );
if ( $post && ! $this->check_update_permission( $post ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
return new WP_Error(
'rest_cannot_edit_others',
__( 'Sorry, you are not allowed to update posts as this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_assign_sticky',
__( 'Sorry, you are not allowed to make posts sticky.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! $this->check_assign_terms_permission( $request ) ) {
return new WP_Error(
'rest_cannot_assign_term',
__( 'Sorry, you are not allowed to assign the provided terms.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$valid_check = $this->get_post( $request['id'] );
if ( is_wp_error( $valid_check ) ) {
return $valid_check;
}
$post_before = get_post( $request['id'] );
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( ! empty( $post->post_status ) ) {
$post_status = $post->post_status;
} else {
$post_status = $post_before->post_status;
}
/*
* `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts.
*
* To ensure that a unique slug is generated, pass the post data with the 'publish' status.
*/
if ( ! empty( $post->post_name ) && in_array( $post_status, array( 'draft', 'pending' ), true ) ) {
$post_parent = ! empty( $post->post_parent ) ? $post->post_parent : 0;
$post->post_name = wp_unique_post_slug(
$post->post_name,
$post->ID,
'publish',
$post->post_type,
$post_parent
);
}
// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
$post_id = wp_update_post( wp_slash( (array) $post ), true, false );
if ( is_wp_error( $post_id ) ) {
if ( 'db_update_error' === $post_id->get_error_code() ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post = get_post( $post_id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_insert_{$this->post_type}", $post, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
set_post_format( $post, $request['format'] );
}
if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
$this->handle_featured_media( $request['featured_media'], $post_id );
}
if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
if ( ! empty( $request['sticky'] ) ) {
stick_post( $post_id );
} else {
unstick_post( $post_id );
}
}
if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
$this->handle_template( $request['template'], $post->ID );
}
$terms_update = $this->handle_terms( $post->ID, $request );
if ( is_wp_error( $terms_update ) ) {
return $terms_update;
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $post->ID );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$post = get_post( $post_id );
$fields_update = $this->update_additional_fields_for_object( $post, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
// Filter is fired in WP_REST_Attachments_Controller subclass.
if ( 'attachment' === $this->post_type ) {
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
wp_after_insert_post( $post, true, $post_before );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a given request has access to delete a post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( $post && ! $this->check_delete_permission( $post ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a single post.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$post = $this->get_post( $request['id'] );
if ( is_wp_error( $post ) ) {
return $post;
}
$id = $post->ID;
$force = (bool) $request['force'];
$supports_trash = ( EMPTY_TRASH_DAYS > 0 );
if ( 'attachment' === $post->post_type ) {
$supports_trash = $supports_trash && MEDIA_TRASH;
}
/**
* Filters whether a post is trashable.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_post_trashable`
* - `rest_page_trashable`
* - `rest_attachment_trashable`
*
* Pass false to disable Trash support for the post.
*
* @since 4.7.0
*
* @param bool $supports_trash Whether the post type support trashing.
* @param WP_Post $post The Post object being considered for trashing support.
*/
$supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );
if ( ! $this->check_delete_permission( $post ) ) {
return new WP_Error(
'rest_user_cannot_delete_post',
__( 'Sorry, you are not allowed to delete this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$request->set_param( 'context', 'edit' );
// If we're forcing, then delete permanently.
if ( $force ) {
$previous = $this->prepare_item_for_response( $post, $request );
$result = wp_delete_post( $id, true );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
// Otherwise, only trash if we haven't already.
if ( 'trash' === $post->post_status ) {
return new WP_Error(
'rest_already_trashed',
__( 'The post has already been deleted.' ),
array( 'status' => 410 )
);
}
/*
* (Note that internally this falls through to `wp_delete_post()`
* if the Trash is disabled.)
*/
$result = wp_trash_post( $id );
$post = get_post( $id );
$response = $this->prepare_item_for_response( $post, $request );
}
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The post cannot be deleted.' ),
array( 'status' => 500 )
);
}
/**
* Fires immediately after a single post is deleted or trashed via the REST API.
*
* They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_delete_post`
* - `rest_delete_page`
* - `rest_delete_attachment`
*
* @since 4.7.0
*
* @param WP_Post $post The deleted or trashed post.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "rest_delete_{$this->post_type}", $post, $response, $request );
return $response;
}
/**
* Determines the allowed query_vars for a get_items() response and prepares
* them for WP_Query.
*
* @since 4.7.0
*
* @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
* @param WP_REST_Request $request Optional. Full details about the request.
* @return array Items query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = array();
foreach ( $prepared_args as $key => $value ) {
/**
* Filters the query_vars used in get_items() for the constructed query.
*
* The dynamic portion of the hook name, `$key`, refers to the query_var key.
*
* @since 4.7.0
*
* @param string $value The query_var value.
*/
$query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}
if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
$query_args['ignore_sticky_posts'] = true;
}
// Map to proper WP_Query orderby param.
if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
$orderby_mappings = array(
'id' => 'ID',
'include' => 'post__in',
'slug' => 'post_name',
'include_slugs' => 'post_name__in',
);
if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
}
}
return $query_args;
}
/**
* Checks the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* @since 4.7.0
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional. Local publication time. Default null.
* @return string|null ISO8601/RFC3339 formatted datetime.
*/
protected function prepare_date_response( $date_gmt, $date = null ) {
// Use the date if passed.
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
// Return null if $date_gmt is empty/zeros.
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
// Return the formatted datetime.
return mysql_to_rfc3339( $date_gmt );
}
/**
* Prepares a single post for create or update.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass();
$current_status = '';
// Post ID.
if ( isset( $request['id'] ) ) {
$existing_post = $this->get_post( $request['id'] );
if ( is_wp_error( $existing_post ) ) {
return $existing_post;
}
$prepared_post->ID = $existing_post->ID;
$current_status = $existing_post->post_status;
}
$schema = $this->get_item_schema();
// Post title.
if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$prepared_post->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$prepared_post->post_title = $request['title']['raw'];
}
}
// Post content.
if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
if ( is_string( $request['content'] ) ) {
$prepared_post->post_content = $request['content'];
} elseif ( isset( $request['content']['raw'] ) ) {
$prepared_post->post_content = $request['content']['raw'];
}
}
// Post excerpt.
if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
if ( is_string( $request['excerpt'] ) ) {
$prepared_post->post_excerpt = $request['excerpt'];
} elseif ( isset( $request['excerpt']['raw'] ) ) {
$prepared_post->post_excerpt = $request['excerpt']['raw'];
}
}
// Post type.
if ( empty( $request['id'] ) ) {
// Creating new post, use default type for the controller.
$prepared_post->post_type = $this->post_type;
} else {
// Updating a post, use previous type.
$prepared_post->post_type = get_post_type( $request['id'] );
}
$post_type = get_post_type_object( $prepared_post->post_type );
// Post status.
if (
! empty( $schema['properties']['status'] ) &&
isset( $request['status'] ) &&
( ! $current_status || $current_status !== $request['status'] )
) {
$status = $this->handle_status_param( $request['status'], $post_type );
if ( is_wp_error( $status ) ) {
return $status;
}
$prepared_post->post_status = $status;
}
// Post date.
if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false;
$date_data = rest_get_date_with_gmt( $request['date'] );
if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) {
list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
$prepared_post->edit_date = true;
}
} elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false;
$date_data = rest_get_date_with_gmt( $request['date_gmt'], true );
if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) {
list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
$prepared_post->edit_date = true;
}
}
/*
* Sending a null date or date_gmt value resets date and date_gmt to their
* default values (`0000-00-00 00:00:00`).
*/
if (
( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) ||
( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] )
) {
$prepared_post->post_date_gmt = null;
$prepared_post->post_date = null;
}
// Post slug.
if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
$prepared_post->post_name = $request['slug'];
}
// Author.
if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
$post_author = (int) $request['author'];
if ( get_current_user_id() !== $post_author ) {
$user_obj = get_userdata( $post_author );
if ( ! $user_obj ) {
return new WP_Error(
'rest_invalid_author',
__( 'Invalid author ID.' ),
array( 'status' => 400 )
);
}
}
$prepared_post->post_author = $post_author;
}
// Post password.
if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
$prepared_post->post_password = $request['password'];
if ( '' !== $request['password'] ) {
if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A post can not be sticky and have a password.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A sticky post can not be password protected.' ),
array( 'status' => 400 )
);
}
}
}
if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
return new WP_Error(
'rest_invalid_field',
__( 'A password protected post can not be set to sticky.' ),
array( 'status' => 400 )
);
}
}
// Parent.
if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
if ( 0 === (int) $request['parent'] ) {
$prepared_post->post_parent = 0;
} else {
$parent = get_post( (int) $request['parent'] );
if ( empty( $parent ) ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post parent ID.' ),
array( 'status' => 400 )
);
}
$prepared_post->post_parent = (int) $parent->ID;
}
}
// Menu order.
if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
$prepared_post->menu_order = (int) $request['menu_order'];
}
// Comment status.
if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
$prepared_post->comment_status = $request['comment_status'];
}
// Ping status.
if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
$prepared_post->ping_status = $request['ping_status'];
}
if ( ! empty( $schema['properties']['template'] ) ) {
// Force template to null so that it can be handled exclusively by the REST controller.
$prepared_post->page_template = null;
}
/**
* Filters a post before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_pre_insert_post`
* - `rest_pre_insert_page`
* - `rest_pre_insert_attachment`
*
* @since 4.7.0
*
* @param stdClass $prepared_post An object representing a single post prepared
* for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
}
/**
* Checks whether the status is valid for the given post.
*
* Allows for sending an update request with the current status, even if that status would not be acceptable.
*
* @since 5.6.0
*
* @param string $status The provided status.
* @param WP_REST_Request $request The request object.
* @param string $param The parameter name.
* @return true|WP_Error True if the status is valid, or WP_Error if not.
*/
public function check_status( $status, $request, $param ) {
if ( $request['id'] ) {
$post = $this->get_post( $request['id'] );
if ( ! is_wp_error( $post ) && $post->post_status === $status ) {
return true;
}
}
$args = $request->get_attributes()['args'][ $param ];
return rest_validate_value_from_schema( $status, $args, $param );
}
/**
* Determines validity and normalizes the given status parameter.
*
* @since 4.7.0
*
* @param string $post_status Post status.
* @param WP_Post_Type $post_type Post type.
* @return string|WP_Error Post status or WP_Error if lacking the proper permission.
*/
protected function handle_status_param( $post_status, $post_type ) {
switch ( $post_status ) {
case 'draft':
case 'pending':
break;
case 'private':
if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_publish',
__( 'Sorry, you are not allowed to create private posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
break;
case 'publish':
case 'future':
if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
return new WP_Error(
'rest_cannot_publish',
__( 'Sorry, you are not allowed to publish posts in this post type.' ),
array( 'status' => rest_authorization_required_code() )
);
}
break;
default:
if ( ! get_post_status_object( $post_status ) ) {
$post_status = 'draft';
}
break;
}
return $post_status;
}
/**
* Determines the featured media based on a request param.
*
* @since 4.7.0
*
* @param int $featured_media Featured Media ID.
* @param int $post_id Post ID.
* @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
*/
protected function handle_featured_media( $featured_media, $post_id ) {
$featured_media = (int) $featured_media;
if ( $featured_media ) {
$result = set_post_thumbnail( $post_id, $featured_media );
if ( $result ) {
return true;
} else {
return new WP_Error(
'rest_invalid_featured_media',
__( 'Invalid featured media ID.' ),
array( 'status' => 400 )
);
}
} else {
return delete_post_thumbnail( $post_id );
}
}
/**
* Checks whether the template is valid for the given post.
*
* @since 4.9.0
*
* @param string $template Page template filename.
* @param WP_REST_Request $request Request.
* @return true|WP_Error True if template is still valid or if the same as existing value, or a WP_Error if template not supported.
*/
public function check_template( $template, $request ) {
if ( ! $template ) {
return true;
}
if ( $request['id'] ) {
$post = get_post( $request['id'] );
$current_template = get_page_template_slug( $request['id'] );
} else {
$post = null;
$current_template = '';
}
// Always allow for updating a post to the same template, even if that template is no longer supported.
if ( $template === $current_template ) {
return true;
}
// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
$allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type );
if ( isset( $allowed_templates[ $template ] ) ) {
return true;
}
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter, 2: List of valid values. */
sprintf( __( '%1$s is not one of %2$s.' ), 'template', implode( ', ', array_keys( $allowed_templates ) ) )
);
}
/**
* Sets the template for a post.
*
* @since 4.7.0
* @since 4.9.0 Added the `$validate` parameter.
*
* @param string $template Page template filename.
* @param int $post_id Post ID.
* @param bool $validate Whether to validate that the template selected is valid.
*/
public function handle_template( $template, $post_id, $validate = false ) {
if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
$template = '';
}
update_post_meta( $post_id, '_wp_page_template', $template );
}
/**
* Updates the post's terms from a REST request.
*
* @since 4.7.0
*
* @param int $post_id The post ID to update the terms form.
* @param WP_REST_Request $request The request object with post and terms data.
* @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null.
*/
protected function handle_terms( $post_id, $request ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( ! isset( $request[ $base ] ) ) {
continue;
}
$result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name );
if ( is_wp_error( $result ) ) {
return $result;
}
}
}
/**
* Checks whether current user can assign all terms sent with the current request.
*
* @since 4.7.0
*
* @param WP_REST_Request $request The request object with post and terms data.
* @return bool Whether the current user can assign the provided terms.
*/
protected function check_assign_terms_permission( $request ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( ! isset( $request[ $base ] ) ) {
continue;
}
foreach ( (array) $request[ $base ] as $term_id ) {
// Invalid terms will be rejected later.
if ( ! get_term( $term_id, $taxonomy->name ) ) {
continue;
}
if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
return false;
}
}
}
return true;
}
/**
* Checks if a given post type can be viewed or managed.
*
* @since 4.7.0
*
* @param WP_Post_Type|string $post_type Post type name or object.
* @return bool Whether the post type is allowed in REST.
*/
protected function check_is_post_type_allowed( $post_type ) {
if ( ! is_object( $post_type ) ) {
$post_type = get_post_type_object( $post_type );
}
if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
return true;
}
return false;
}
/**
* Checks if a post can be read.
*
* Correctly handles posts with the inherit status.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be read.
*/
public function check_read_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
// Is the post readable?
if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) {
return true;
}
$post_status_obj = get_post_status_object( $post->post_status );
if ( $post_status_obj && $post_status_obj->public ) {
return true;
}
// Can we read the parent if we're inheriting?
if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
$parent = get_post( $post->post_parent );
if ( $parent ) {
return $this->check_read_permission( $parent );
}
}
/*
* If there isn't a parent, but the status is set to inherit, assume
* it's published (as per get_post_status()).
*/
if ( 'inherit' === $post->post_status ) {
return true;
}
return false;
}
/**
* Checks if a post can be edited.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be edited.
*/
protected function check_update_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( 'edit_post', $post->ID );
}
/**
* Checks if a post can be created.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be created.
*/
protected function check_create_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( $post_type->cap->create_posts );
}
/**
* Checks if a post can be deleted.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return bool Whether the post can be deleted.
*/
protected function check_delete_permission( $post ) {
$post_type = get_post_type_object( $post->post_type );
if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
return false;
}
return current_user_can( 'delete_post', $post->ID );
}
/**
* Prepares a single post output for response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @global WP_Post $post Global post object.
*
* @param WP_Post $item Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$GLOBALS['post'] = $post;
setup_postdata( $post );
$fields = $this->get_fields_for_response( $request );
// Base fields for every post.
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}
if ( rest_is_field_included( 'date', $fields ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}
if ( rest_is_field_included( 'date_gmt', $fields ) ) {
/*
* For drafts, `post_date_gmt` may not be set, indicating that the date
* of the draft should be updated each time it is saved (see #38883).
* In this case, shim the value based on the `post_date` field
* with the site's timezone offset applied.
*/
if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
$post_date_gmt = get_gmt_from_date( $post->post_date );
} else {
$post_date_gmt = $post->post_date_gmt;
}
$data['date_gmt'] = $this->prepare_date_response( $post_date_gmt );
}
if ( rest_is_field_included( 'guid', $fields ) ) {
$data['guid'] = array(
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
'raw' => $post->guid,
);
}
if ( rest_is_field_included( 'modified', $fields ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}
if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
/*
* For drafts, `post_modified_gmt` may not be set (see `post_date_gmt` comments
* above). In this case, shim the value based on the `post_modified` field
* with the site's timezone offset applied.
*/
if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
$post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) );
} else {
$post_modified_gmt = $post->post_modified_gmt;
}
$data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt );
}
if ( rest_is_field_included( 'password', $fields ) ) {
$data['password'] = $post->post_password;
}
if ( rest_is_field_included( 'slug', $fields ) ) {
$data['slug'] = $post->post_name;
}
if ( rest_is_field_included( 'status', $fields ) ) {
$data['status'] = $post->post_status;
}
if ( rest_is_field_included( 'type', $fields ) ) {
$data['type'] = $post->post_type;
}
if ( rest_is_field_included( 'link', $fields ) ) {
$data['link'] = get_permalink( $post->ID );
}
if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
$data['title']['rendered'] = get_the_title( $post->ID );
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}
$has_password_filter = false;
if ( $this->can_access_password_content( $post, $request ) ) {
$this->password_check_passed[ $post->ID ] = true;
// Allow access to the post, permissions already checked before.
add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
$has_password_filter = true;
}
if ( rest_is_field_included( 'content', $fields ) ) {
$data['content'] = array();
}
if ( rest_is_field_included( 'content.raw', $fields ) ) {
$data['content']['raw'] = $post->post_content;
}
if ( rest_is_field_included( 'content.rendered', $fields ) ) {
/** This filter is documented in wp-includes/post-template.php */
$data['content']['rendered'] = post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content );
}
if ( rest_is_field_included( 'content.protected', $fields ) ) {
$data['content']['protected'] = (bool) $post->post_password;
}
if ( rest_is_field_included( 'content.block_version', $fields ) ) {
$data['content']['block_version'] = block_version( $post->post_content );
}
if ( rest_is_field_included( 'excerpt', $fields ) ) {
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'the_excerpt', $excerpt );
$data['excerpt'] = array(
'raw' => $post->post_excerpt,
'rendered' => post_password_required( $post ) ? '' : $excerpt,
'protected' => (bool) $post->post_password,
);
}
if ( $has_password_filter ) {
// Reset filter.
remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
}
if ( rest_is_field_included( 'author', $fields ) ) {
$data['author'] = (int) $post->post_author;
}
if ( rest_is_field_included( 'featured_media', $fields ) ) {
$data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
}
if ( rest_is_field_included( 'parent', $fields ) ) {
$data['parent'] = (int) $post->post_parent;
}
if ( rest_is_field_included( 'menu_order', $fields ) ) {
$data['menu_order'] = (int) $post->menu_order;
}
if ( rest_is_field_included( 'comment_status', $fields ) ) {
$data['comment_status'] = $post->comment_status;
}
if ( rest_is_field_included( 'ping_status', $fields ) ) {
$data['ping_status'] = $post->ping_status;
}
if ( rest_is_field_included( 'sticky', $fields ) ) {
$data['sticky'] = is_sticky( $post->ID );
}
if ( rest_is_field_included( 'template', $fields ) ) {
$template = get_page_template_slug( $post->ID );
if ( $template ) {
$data['template'] = $template;
} else {
$data['template'] = '';
}
}
if ( rest_is_field_included( 'format', $fields ) ) {
$data['format'] = get_post_format( $post->ID );
// Fill in blank post format.
if ( empty( $data['format'] ) ) {
$data['format'] = 'standard';
}
}
if ( rest_is_field_included( 'meta', $fields ) ) {
$data['meta'] = $this->meta->get_value( $post->ID, $request );
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( rest_is_field_included( $base, $fields ) ) {
$terms = get_the_terms( $post, $taxonomy->name );
$data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
}
}
$post_type_obj = get_post_type_object( $post->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$permalink_template_requested = rest_is_field_included( 'permalink_template', $fields );
$generated_slug_requested = rest_is_field_included( 'generated_slug', $fields );
if ( $permalink_template_requested || $generated_slug_requested ) {
if ( ! function_exists( 'get_sample_permalink' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$sample_permalink = get_sample_permalink( $post->ID, $post->post_title, '' );
if ( $permalink_template_requested ) {
$data['permalink_template'] = $sample_permalink[0];
}
if ( $generated_slug_requested ) {
$data['generated_slug'] = $sample_permalink[1];
}
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $post );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions( $post, $request );
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}
/**
* Filters the post data for a REST API response.
*
* The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
*
* Possible hook names include:
*
* - `rest_prepare_post`
* - `rest_prepare_page`
* - `rest_prepare_attachment`
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Overwrites the default protected title format.
*
* By default, WordPress will show password protected posts with a title of
* "Protected: %s", as the REST API communicates the protected status of a post
* in a machine readable format, we remove the "Protected: " prefix.
*
* @since 4.7.0
*
* @return string Protected title format.
*/
public function protected_title_format() {
return '%s';
}
/**
* Prepares links for the request.
*
* @since 4.7.0
*
* @param WP_Post $post Post object.
* @return array Links for the given post.
*/
protected function prepare_links( $post ) {
// Entity meta.
$links = array(
'self' => array(
'href' => rest_url( rest_get_route_for_post( $post->ID ) ),
),
'collection' => array(
'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
),
'about' => array(
'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
),
);
if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
&& ! empty( $post->post_author ) ) {
$links['author'] = array(
'href' => rest_url( 'wp/v2/users/' . $post->post_author ),
'embeddable' => true,
);
}
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
$replies_url = rest_url( 'wp/v2/comments' );
$replies_url = add_query_arg( 'post', $post->ID, $replies_url );
$links['replies'] = array(
'href' => $replies_url,
'embeddable' => true,
);
}
if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
$revisions = wp_get_latest_revision_id_and_total_count( $post->ID );
$revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
$revisions_base = sprintf( '/%s/%s/%d/revisions', $this->namespace, $this->rest_base, $post->ID );
$links['version-history'] = array(
'href' => rest_url( $revisions_base ),
'count' => $revisions_count,
);
if ( $revisions_count > 0 ) {
$links['predecessor-version'] = array(
'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ),
'id' => $revisions['latest_id'],
);
}
}
$post_type_obj = get_post_type_object( $post->post_type );
if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
$links['up'] = array(
'href' => rest_url( rest_get_route_for_post( $post->post_parent ) ),
'embeddable' => true,
);
}
// If we have a featured media, add that.
$featured_media = get_post_thumbnail_id( $post->ID );
if ( $featured_media ) {
$image_url = rest_url( rest_get_route_for_post( $featured_media ) );
$links['https://api.w.org/featuredmedia'] = array(
'href' => $image_url,
'embeddable' => true,
);
}
if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
$attachments_url = rest_url( rest_get_route_for_post_type_items( 'attachment' ) );
$attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );
$links['https://api.w.org/attachment'] = array(
'href' => $attachments_url,
);
}
$taxonomies = get_object_taxonomies( $post->post_type );
if ( ! empty( $taxonomies ) ) {
$links['https://api.w.org/term'] = array();
foreach ( $taxonomies as $tax ) {
$taxonomy_route = rest_get_route_for_taxonomy_items( $tax );
// Skip taxonomies that are not public.
if ( empty( $taxonomy_route ) ) {
continue;
}
$terms_url = add_query_arg(
'post',
$post->ID,
rest_url( $taxonomy_route )
);
$links['https://api.w.org/term'][] = array(
'href' => $terms_url,
'taxonomy' => $tax,
'embeddable' => true,
);
}
}
return $links;
}
/**
* Gets the link relations available for the post and current user.
*
* @since 4.9.8
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return array List of link relations.
*/
protected function get_available_actions( $post, $request ) {
if ( 'edit' !== $request['context'] ) {
return array();
}
$rels = array();
$post_type = get_post_type_object( $post->post_type );
if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-publish';
}
if ( current_user_can( 'unfiltered_html' ) ) {
$rels[] = 'https://api.w.org/action-unfiltered-html';
}
if ( 'post' === $post_type->name ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-sticky';
}
}
if ( post_type_supports( $post_type->name, 'author' ) ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
$rels[] = 'https://api.w.org/action-assign-author';
}
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $tax ) {
$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
$create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms;
if ( current_user_can( $create_cap ) ) {
$rels[] = 'https://api.w.org/action-create-' . $tax_base;
}
if ( current_user_can( $tax->cap->assign_terms ) ) {
$rels[] = 'https://api.w.org/action-assign-' . $tax_base;
}
}
return $rels;
}
/**
* Retrieves the post's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
// Base properties for every Post.
'properties' => array(
'date' => array(
'description' => __( "The date the post was published, in the site's timezone." ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the post was published, as GMT.' ),
'type' => array( 'string', 'null' ),
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'guid' => array(
'description' => __( 'The globally unique identifier for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'GUID for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'rendered' => array(
'description' => __( 'GUID for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
'id' => array(
'description' => __( 'Unique identifier for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'link' => array(
'description' => __( 'URL to the post.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'modified' => array(
'description' => __( "The date the post was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'modified_gmt' => array(
'description' => __( 'The date the post was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the post unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'status' => array(
'description' => __( 'A named status for the post.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'validate_callback' => array( $this, 'check_status' ),
),
),
'type' => array(
'description' => __( 'Type of post.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'password' => array(
'description' => __( 'A password to protect access to the content and excerpt.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
),
);
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$schema['properties']['permalink_template'] = array(
'description' => __( 'Permalink template for the post.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['generated_slug'] = array(
'description' => __( 'Slug automatically generated from the post title.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
}
if ( $post_type_obj->hierarchical ) {
$schema['properties']['parent'] = array(
'description' => __( 'The ID for the parent of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
}
$post_type_attributes = array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'page-attributes',
'post-formats',
'custom-fields',
);
$fixed_schemas = array(
'post' => array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'post-formats',
'custom-fields',
),
'page' => array(
'title',
'editor',
'author',
'excerpt',
'thumbnail',
'comments',
'revisions',
'page-attributes',
'custom-fields',
),
'attachment' => array(
'title',
'author',
'comments',
'revisions',
'custom-fields',
'thumbnail',
),
);
foreach ( $post_type_attributes as $attribute ) {
if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
continue;
} elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
continue;
}
switch ( $attribute ) {
case 'title':
$schema['properties']['title'] = array(
'description' => __( 'The title for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'editor':
$schema['properties']['content'] = array(
'description' => __( 'The content for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Content for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML content for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'block_version' => array(
'description' => __( 'Version of the content block format used by the post.' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether the content is protected with a password.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'author':
$schema['properties']['author'] = array(
'description' => __( 'The ID for the author of the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
break;
case 'excerpt':
$schema['properties']['excerpt'] = array(
'description' => __( 'The excerpt for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Excerpt for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML excerpt for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'protected' => array(
'description' => __( 'Whether the excerpt is protected with a password.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
break;
case 'thumbnail':
$schema['properties']['featured_media'] = array(
'description' => __( 'The ID of the featured media for the post.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
break;
case 'comments':
$schema['properties']['comment_status'] = array(
'description' => __( 'Whether or not comments are open on the post.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['ping_status'] = array(
'description' => __( 'Whether or not the post can be pinged.' ),
'type' => 'string',
'enum' => array( 'open', 'closed' ),
'context' => array( 'view', 'edit' ),
);
break;
case 'page-attributes':
$schema['properties']['menu_order'] = array(
'description' => __( 'The order of the post in relation to other posts.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
break;
case 'post-formats':
// Get the native post formats and remove the array keys.
$formats = array_values( get_post_format_slugs() );
$schema['properties']['format'] = array(
'description' => __( 'The format for the post.' ),
'type' => 'string',
'enum' => $formats,
'context' => array( 'view', 'edit' ),
);
break;
case 'custom-fields':
$schema['properties']['meta'] = $this->meta->get_field_schema();
break;
}
}
if ( 'post' === $this->post_type ) {
$schema['properties']['sticky'] = array(
'description' => __( 'Whether or not the post should be treated as sticky.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
);
}
$schema['properties']['template'] = array(
'description' => __( 'The theme file to use to display the post.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'validate_callback' => array( $this, 'check_template' ),
),
);
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
if ( array_key_exists( $base, $schema['properties'] ) ) {
$taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name';
_doing_it_wrong(
'register_taxonomy',
sprintf(
/* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */
__( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ),
$taxonomy->name,
$taxonomy_field_name_with_conflict,
$base
),
'5.4.0'
);
}
$schema['properties'][ $base ] = array(
/* translators: %s: Taxonomy name. */
'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
);
}
$schema_links = $this->get_schema_links();
if ( $schema_links ) {
$schema['links'] = $schema_links;
}
// Take a snapshot of which fields are in the schema pre-filtering.
$schema_fields = array_keys( $schema['properties'] );
/**
* Filters the post's schema.
*
* The dynamic portion of the filter, `$this->post_type`, refers to the
* post type slug for the controller.
*
* Possible hook names include:
*
* - `rest_post_item_schema`
* - `rest_page_item_schema`
* - `rest_attachment_item_schema`
*
* @since 5.4.0
*
* @param array $schema Item schema data.
*/
$schema = apply_filters( "rest_{$this->post_type}_item_schema", $schema );
// Emit a _doing_it_wrong warning if user tries to add new properties using this filter.
$new_fields = array_diff( array_keys( $schema['properties'] ), $schema_fields );
if ( count( $new_fields ) > 0 ) {
_doing_it_wrong(
__METHOD__,
sprintf(
/* translators: %s: register_rest_field */
__( 'Please use %s to add new schema properties.' ),
'register_rest_field'
),
'5.4.0'
);
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves Link Description Objects that should be added to the Schema for the posts collection.
*
* @since 4.9.8
*
* @return array
*/
protected function get_schema_links() {
$href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
$links = array();
if ( 'attachment' !== $this->post_type ) {
$links[] = array(
'rel' => 'https://api.w.org/action-publish',
'title' => __( 'The current user can publish this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'status' => array(
'type' => 'string',
'enum' => array( 'publish', 'future' ),
),
),
),
);
}
$links[] = array(
'rel' => 'https://api.w.org/action-unfiltered-html',
'title' => __( 'The current user can post unfiltered HTML markup and JavaScript.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'content' => array(
'raw' => array(
'type' => 'string',
),
),
),
),
);
if ( 'post' === $this->post_type ) {
$links[] = array(
'rel' => 'https://api.w.org/action-sticky',
'title' => __( 'The current user can sticky this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'sticky' => array(
'type' => 'boolean',
),
),
),
);
}
if ( post_type_supports( $this->post_type, 'author' ) ) {
$links[] = array(
'rel' => 'https://api.w.org/action-assign-author',
'title' => __( 'The current user can change the author on this post.' ),
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
'author' => array(
'type' => 'integer',
),
),
),
);
}
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
foreach ( $taxonomies as $tax ) {
$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
/* translators: %s: Taxonomy name. */
$assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name );
/* translators: %s: Taxonomy name. */
$create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name );
$links[] = array(
'rel' => 'https://api.w.org/action-assign-' . $tax_base,
'title' => $assign_title,
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
$tax_base => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
),
),
);
$links[] = array(
'rel' => 'https://api.w.org/action-create-' . $tax_base,
'title' => $create_title,
'href' => $href,
'targetSchema' => array(
'type' => 'object',
'properties' => array(
$tax_base => array(
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
),
),
);
}
return $links;
}
/**
* Retrieves the query params for the posts collection.
*
* @since 4.7.0
* @since 5.4.0 The `tax_relation` query parameter was added.
* @since 5.7.0 The `modified_after` and `modified_before` query parameters were added.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['after'] = array(
'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['modified_after'] = array(
'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
if ( post_type_supports( $this->post_type, 'author' ) ) {
$query_params['author'] = array(
'description' => __( 'Limit result set to posts assigned to specific authors.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['author_exclude'] = array(
'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
}
$query_params['before'] = array(
'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['modified_before'] = array(
'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ),
'type' => 'string',
'format' => 'date-time',
);
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
$query_params['menu_order'] = array(
'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
'type' => 'integer',
);
}
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by post attribute.' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'author',
'date',
'id',
'include',
'modified',
'parent',
'relevance',
'slug',
'include_slugs',
'title',
),
);
if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
$query_params['orderby']['enum'][] = 'menu_order';
}
$post_type = get_post_type_object( $this->post_type );
if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
$query_params['parent'] = array(
'description' => __( 'Limit result set to items with particular parent IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
}
$query_params['search_columns'] = array(
'default' => array(),
'description' => __( 'Array of column names to be searched.' ),
'type' => 'array',
'items' => array(
'enum' => array( 'post_title', 'post_content', 'post_excerpt' ),
'type' => 'string',
),
);
$query_params['slug'] = array(
'description' => __( 'Limit result set to posts with one or more specific slugs.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
$query_params['status'] = array(
'default' => 'publish',
'description' => __( 'Limit result set to posts assigned one or more statuses.' ),
'type' => 'array',
'items' => array(
'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
'type' => 'string',
),
'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
);
$query_params = $this->prepare_taxonomy_limit_schema( $query_params );
if ( 'post' === $this->post_type ) {
$query_params['sticky'] = array(
'description' => __( 'Limit result set to items that are sticky.' ),
'type' => 'boolean',
);
}
/**
* Filters collection parameters for the posts controller.
*
* The dynamic part of the filter `$this->post_type` refers to the post
* type slug for the controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Query parameter. Use the
* `rest_{$this->post_type}_query` filter to set WP_Query parameters.
*
* @since 4.7.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @param WP_Post_Type $post_type Post type object.
*/
return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type );
}
/**
* Sanitizes and validates the list of post statuses, including whether the
* user can query private statuses.
*
* @since 4.7.0
*
* @param string|array $statuses One or more post statuses.
* @param WP_REST_Request $request Full details about the request.
* @param string $parameter Additional parameter to pass to validation.
* @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
*/
public function sanitize_post_statuses( $statuses, $request, $parameter ) {
$statuses = wp_parse_slug_list( $statuses );
// The default status is different in WP_REST_Attachments_Controller.
$attributes = $request->get_attributes();
$default_status = $attributes['args']['status']['default'];
foreach ( $statuses as $status ) {
if ( $status === $default_status ) {
continue;
}
$post_type_obj = get_post_type_object( $this->post_type );
if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) {
$result = rest_validate_request_arg( $status, $request, $parameter );
if ( is_wp_error( $result ) ) {
return $result;
}
} else {
return new WP_Error(
'rest_forbidden_status',
__( 'Status is forbidden.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
return $statuses;
}
/**
* Prepares the 'tax_query' for a collection of posts.
*
* @since 5.7.0
*
* @param array $args WP_Query arguments.
* @param WP_REST_Request $request Full details about the request.
* @return array Updated query arguments.
*/
private function prepare_tax_query( array $args, WP_REST_Request $request ) {
$relation = $request['tax_relation'];
if ( $relation ) {
$args['tax_query'] = array( 'relation' => $relation );
}
$taxonomies = wp_list_filter(
get_object_taxonomies( $this->post_type, 'objects' ),
array( 'show_in_rest' => true )
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$tax_include = $request[ $base ];
$tax_exclude = $request[ $base . '_exclude' ];
if ( $tax_include ) {
$terms = array();
$include_children = false;
$operator = 'IN';
if ( rest_is_array( $tax_include ) ) {
$terms = $tax_include;
} elseif ( rest_is_object( $tax_include ) ) {
$terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
$include_children = ! empty( $tax_include['include_children'] );
if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) {
$operator = 'AND';
}
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
'operator' => $operator,
);
}
}
if ( $tax_exclude ) {
$terms = array();
$include_children = false;
if ( rest_is_array( $tax_exclude ) ) {
$terms = $tax_exclude;
} elseif ( rest_is_object( $tax_exclude ) ) {
$terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
$include_children = ! empty( $tax_exclude['include_children'] );
}
if ( $terms ) {
$args['tax_query'][] = array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $terms,
'include_children' => $include_children,
'operator' => 'NOT IN',
);
}
}
}
return $args;
}
/**
* Prepares the collection schema for including and excluding items by terms.
*
* @since 5.7.0
*
* @param array $query_params Collection schema.
* @return array Updated schema.
*/
private function prepare_taxonomy_limit_schema( array $query_params ) {
$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
if ( ! $taxonomies ) {
return $query_params;
}
$query_params['tax_relation'] = array(
'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
);
$limit_schema = array(
'type' => array( 'object', 'array' ),
'oneOf' => array(
array(
'title' => __( 'Term ID List' ),
'description' => __( 'Match terms with the listed IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
),
array(
'title' => __( 'Term ID Taxonomy Query' ),
'description' => __( 'Perform an advanced term query.' ),
'type' => 'object',
'properties' => array(
'terms' => array(
'description' => __( 'Term IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
),
'include_children' => array(
'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
'type' => 'boolean',
'default' => false,
),
),
'additionalProperties' => false,
),
),
);
$include_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
// 'operator' is supported only for 'include' queries.
$include_schema['oneOf'][1]['properties']['operator'] = array(
'description' => __( 'Whether items must be assigned all or any of the specified terms.' ),
'type' => 'string',
'enum' => array( 'AND', 'OR' ),
'default' => 'OR',
);
$exclude_schema = array_merge(
array(
/* translators: %s: Taxonomy name. */
'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
),
$limit_schema
);
foreach ( $taxonomies as $taxonomy ) {
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$base_exclude = $base . '_exclude';
$query_params[ $base ] = $include_schema;
$query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );
$query_params[ $base_exclude ] = $exclude_schema;
$query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );
if ( ! $taxonomy->hierarchical ) {
unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
}
}
return $query_params;
}
}
endpoints/class-wp-rest-revisions-controller.php 0000644 00000062170 15153067731 0016202 0 ustar 00 <?php
/**
* REST API: WP_REST_Revisions_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to access revisions via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Revisions_Controller extends WP_REST_Controller {
/**
* Parent post type.
*
* @since 4.7.0
* @var string
*/
private $parent_post_type;
/**
* Instance of a revision meta fields object.
*
* @since 6.4.0
* @var WP_REST_Post_Meta_Fields
*/
protected $meta;
/**
* Parent controller.
*
* @since 4.7.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* The base of the parent controller's route.
*
* @since 4.7.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
$this->parent_post_type = $parent_post_type;
$post_type_object = get_post_type_object( $parent_post_type );
$parent_controller = $post_type_object->get_rest_controller();
if ( ! $parent_controller ) {
$parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
}
$this->parent_controller = $parent_controller;
$this->rest_base = 'revisions';
$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
$this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
$this->meta = new WP_REST_Post_Meta_Fields( $parent_post_type );
}
/**
* Registers the routes for revisions based on post types supporting revisions.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base,
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'parent' => array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the revision.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Required to be true, as revisions do not support trashing.' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get the parent post, if the ID is valid.
*
* @since 4.7.2
*
* @param int $parent_post_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent( $parent_post_id ) {
$error = new WP_Error(
'rest_post_invalid_parent',
__( 'Invalid post parent ID.' ),
array( 'status' => 404 )
);
if ( (int) $parent_post_id <= 0 ) {
return $error;
}
$parent_post = get_post( (int) $parent_post_id );
if ( empty( $parent_post ) || empty( $parent_post->ID )
|| $this->parent_post_type !== $parent_post->post_type
) {
return $error;
}
return $parent_post;
}
/**
* Checks if a given request has access to get revisions.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
if ( ! current_user_can( 'edit_post', $parent->ID ) ) {
return new WP_Error(
'rest_cannot_read',
__( 'Sorry, you are not allowed to view revisions of this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Get the revision, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
*/
protected function get_revision( $id ) {
$error = new WP_Error(
'rest_post_invalid_id',
__( 'Invalid revision ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$revision = get_post( (int) $id );
if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
return $error;
}
return $revision;
}
/**
* Gets a collection of revisions.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
// Ensure a search string is set in case the orderby is set to 'relevance'.
if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
return new WP_Error(
'rest_no_search_term_defined',
__( 'You need to define a search term to order by relevance.' ),
array( 'status' => 400 )
);
}
// Ensure an include parameter is set in case the orderby is set to 'include'.
if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
return new WP_Error(
'rest_orderby_include_missing_include',
__( 'You need to define an include parameter to order by include.' ),
array( 'status' => 400 )
);
}
if ( wp_revisions_enabled( $parent ) ) {
$registered = $this->get_collection_params();
$args = array(
'post_parent' => $parent->ID,
'post_type' => 'revision',
'post_status' => 'inherit',
'posts_per_page' => -1,
'orderby' => 'date ID',
'order' => 'DESC',
'suppress_filters' => true,
);
$parameter_mappings = array(
'exclude' => 'post__not_in',
'include' => 'post__in',
'offset' => 'offset',
'order' => 'order',
'orderby' => 'orderby',
'page' => 'paged',
'per_page' => 'posts_per_page',
'search' => 's',
);
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$args[ $wp_param ] = $request[ $api_param ];
}
}
// For backward-compatibility, 'date' needs to resolve to 'date ID'.
if ( isset( $args['orderby'] ) && 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';
}
/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
$args = apply_filters( 'rest_revision_query', $args, $request );
$query_args = $this->prepare_items_query( $args, $request );
$revisions_query = new WP_Query();
$revisions = $revisions_query->query( $query_args );
$offset = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0;
$page = (int) $query_args['paged'];
$total_revisions = $revisions_query->found_posts;
if ( $total_revisions < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'], $query_args['offset'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_revisions = $count_query->found_posts;
}
if ( $revisions_query->query_vars['posts_per_page'] > 0 ) {
$max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] );
} else {
$max_pages = $total_revisions > 0 ? 1 : 0;
}
if ( $total_revisions > 0 ) {
if ( $offset >= $total_revisions ) {
return new WP_Error(
'rest_revision_invalid_offset_number',
__( 'The offset number requested is larger than or equal to the number of available revisions.' ),
array( 'status' => 400 )
);
} elseif ( ! $offset && $page > $max_pages ) {
return new WP_Error(
'rest_revision_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
}
} else {
$revisions = array();
$total_revisions = 0;
$max_pages = 0;
$page = (int) $request['page'];
}
$response = array();
foreach ( $revisions as $revision ) {
$data = $this->prepare_item_for_response( $revision, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $response );
$response->header( 'X-WP-Total', (int) $total_revisions );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$request_params = $request->get_query_params();
$base_path = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) );
$base = add_query_arg( urlencode_deep( $request_params ), $base_path );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Checks if a given request has access to get a specific revision.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
return $this->get_items_permissions_check( $request );
}
/**
* Retrieves one revision from the collection.
*
* @since 4.7.0
* @since 6.5.0 Added a condition to check that parent id matches revision parent id.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
$revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) {
return $revision;
}
if ( (int) $parent->ID !== (int) $revision->post_parent ) {
return new WP_Error(
'rest_revision_parent_id_mismatch',
/* translators: %d: A post id. */
sprintf( __( 'The revision does not belong to the specified parent with id of "%d"' ), $parent->ID ),
array( 'status' => 404 )
);
}
$response = $this->prepare_item_for_response( $revision, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a given request has access to delete a revision.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
if ( ! current_user_can( 'delete_post', $parent->ID ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete revisions of this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) {
return $revision;
}
$response = $this->get_items_permissions_check( $request );
if ( ! $response || is_wp_error( $response ) ) {
return $response;
}
if ( ! current_user_can( 'delete_post', $revision->ID ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this revision.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a single revision.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) {
return $revision;
}
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for revisions.
if ( ! $force ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "Revisions do not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
$previous = $this->prepare_item_for_response( $revision, $request );
$result = wp_delete_post( $request['id'], true );
/**
* Fires after a revision is deleted via the REST API.
*
* @since 4.7.0
*
* @param WP_Post|false|null $result The revision object (if it was deleted or moved to the Trash successfully)
* or false or null (failure). If the revision was moved to the Trash, $result represents
* its new state; if it was deleted, $result represents its state before deletion.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_revision', $result, $request );
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The post cannot be deleted.' ),
array( 'status' => 500 )
);
}
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
return $response;
}
/**
* Determines the allowed query_vars for a get_items() response and prepares
* them for WP_Query.
*
* @since 5.0.0
*
* @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
* @param WP_REST_Request $request Optional. Full details about the request.
* @return array Items query arguments.
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$query_args = array();
foreach ( $prepared_args as $key => $value ) {
/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
$query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
}
// Map to proper WP_Query orderby param.
if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
$orderby_mappings = array(
'id' => 'ID',
'include' => 'post__in',
'slug' => 'post_name',
'include_slugs' => 'post_name__in',
);
if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
}
}
return $query_args;
}
/**
* Prepares the revision for the REST response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
*
* @global WP_Post $post Global post object.
*
* @param WP_Post $item Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$post = $item;
$GLOBALS['post'] = $post;
setup_postdata( $post );
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'author', $fields, true ) ) {
$data['author'] = (int) $post->post_author;
}
if ( in_array( 'date', $fields, true ) ) {
$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
}
if ( in_array( 'date_gmt', $fields, true ) ) {
$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
}
if ( in_array( 'id', $fields, true ) ) {
$data['id'] = $post->ID;
}
if ( in_array( 'modified', $fields, true ) ) {
$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
}
if ( in_array( 'modified_gmt', $fields, true ) ) {
$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
}
if ( in_array( 'parent', $fields, true ) ) {
$data['parent'] = (int) $post->post_parent;
}
if ( in_array( 'slug', $fields, true ) ) {
$data['slug'] = $post->post_name;
}
if ( in_array( 'guid', $fields, true ) ) {
$data['guid'] = array(
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
'raw' => $post->guid,
);
}
if ( in_array( 'title', $fields, true ) ) {
$data['title'] = array(
'raw' => $post->post_title,
'rendered' => get_the_title( $post->ID ),
);
}
if ( in_array( 'content', $fields, true ) ) {
$data['content'] = array(
'raw' => $post->post_content,
/** This filter is documented in wp-includes/post-template.php */
'rendered' => apply_filters( 'the_content', $post->post_content ),
);
}
if ( in_array( 'excerpt', $fields, true ) ) {
$data['excerpt'] = array(
'raw' => $post->post_excerpt,
'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
);
}
if ( rest_is_field_included( 'meta', $fields ) ) {
$data['meta'] = $this->meta->get_value( $post->ID, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( ! empty( $data['parent'] ) ) {
$response->add_link( 'parent', rest_url( rest_get_route_for_post( $data['parent'] ) ) );
}
/**
* Filters a revision returned from the REST API.
*
* Allows modification of the revision right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post The original revision object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_revision', $response, $post, $request );
}
/**
* Checks the post_date_gmt or modified_gmt and prepare any post or
* modified date for single post output.
*
* @since 4.7.0
*
* @param string $date_gmt GMT publication time.
* @param string|null $date Optional. Local publication time. Default null.
* @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
*/
protected function prepare_date_response( $date_gmt, $date = null ) {
if ( '0000-00-00 00:00:00' === $date_gmt ) {
return null;
}
if ( isset( $date ) ) {
return mysql_to_rfc3339( $date );
}
return mysql_to_rfc3339( $date_gmt );
}
/**
* Retrieves the revision's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => "{$this->parent_post_type}-revision",
'type' => 'object',
// Base properties for every Revision.
'properties' => array(
'author' => array(
'description' => __( 'The ID for the author of the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'date' => array(
'description' => __( "The date the revision was published, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit', 'embed' ),
),
'date_gmt' => array(
'description' => __( 'The date the revision was published, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'guid' => array(
'description' => __( 'GUID for the revision, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'id' => array(
'description' => __( 'Unique identifier for the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'modified' => array(
'description' => __( "The date the revision was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'modified_gmt' => array(
'description' => __( 'The date the revision was last modified, as GMT.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'parent' => array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the revision unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
),
);
$parent_schema = $this->parent_controller->get_item_schema();
if ( ! empty( $parent_schema['properties']['title'] ) ) {
$schema['properties']['title'] = $parent_schema['properties']['title'];
}
if ( ! empty( $parent_schema['properties']['content'] ) ) {
$schema['properties']['content'] = $parent_schema['properties']['content'];
}
if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
$schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
}
if ( ! empty( $parent_schema['properties']['guid'] ) ) {
$schema['properties']['guid'] = $parent_schema['properties']['guid'];
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
unset( $query_params['per_page']['default'] );
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'id',
'include',
'relevance',
'slug',
'include_slugs',
'title',
),
);
return $query_params;
}
/**
* Checks the post excerpt and prepare it for single post output.
*
* @since 4.7.0
*
* @param string $excerpt The post excerpt.
* @param WP_Post $post Post revision object.
* @return string Prepared excerpt or empty string.
*/
protected function prepare_excerpt_response( $excerpt, $post ) {
/** This filter is documented in wp-includes/post-template.php */
$excerpt = apply_filters( 'the_excerpt', $excerpt, $post );
if ( empty( $excerpt ) ) {
return '';
}
return $excerpt;
}
}
endpoints/class-wp-rest-search-controller.php 0000644 00000026061 15153067731 0015425 0 ustar 00 <?php
/**
* REST API: WP_REST_Search_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core class to search through all WordPress content via the REST API.
*
* @since 5.0.0
*
* @see WP_REST_Controller
*/
class WP_REST_Search_Controller extends WP_REST_Controller {
/**
* ID property name.
*/
const PROP_ID = 'id';
/**
* Title property name.
*/
const PROP_TITLE = 'title';
/**
* URL property name.
*/
const PROP_URL = 'url';
/**
* Type property name.
*/
const PROP_TYPE = 'type';
/**
* Subtype property name.
*/
const PROP_SUBTYPE = 'subtype';
/**
* Identifier for the 'any' type.
*/
const TYPE_ANY = 'any';
/**
* Search handlers used by the controller.
*
* @since 5.0.0
* @var WP_REST_Search_Handler[]
*/
protected $search_handlers = array();
/**
* Constructor.
*
* @since 5.0.0
*
* @param array $search_handlers List of search handlers to use in the controller. Each search
* handler instance must extend the `WP_REST_Search_Handler` class.
*/
public function __construct( array $search_handlers ) {
$this->namespace = 'wp/v2';
$this->rest_base = 'search';
foreach ( $search_handlers as $search_handler ) {
if ( ! $search_handler instanceof WP_REST_Search_Handler ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: PHP class name. */
sprintf( __( 'REST search handlers must extend the %s class.' ), 'WP_REST_Search_Handler' ),
'5.0.0'
);
continue;
}
$this->search_handlers[ $search_handler->get_type() ] = $search_handler;
}
}
/**
* Registers the routes for the search controller.
*
* @since 5.0.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permission_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to search content.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has search access, WP_Error object otherwise.
*/
public function get_items_permission_check( $request ) {
return true;
}
/**
* Retrieves a collection of search results.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$handler = $this->get_search_handler( $request );
if ( is_wp_error( $handler ) ) {
return $handler;
}
$result = $handler->search_items( $request );
if ( ! isset( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! is_array( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! isset( $result[ WP_REST_Search_Handler::RESULT_TOTAL ] ) ) {
return new WP_Error(
'rest_search_handler_error',
__( 'Internal search handler error.' ),
array( 'status' => 500 )
);
}
$ids = $result[ WP_REST_Search_Handler::RESULT_IDS ];
$results = array();
foreach ( $ids as $id ) {
$data = $this->prepare_item_for_response( $id, $request );
$results[] = $this->prepare_response_for_collection( $data );
}
$total = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ];
$page = (int) $request['page'];
$per_page = (int) $request['per_page'];
$max_pages = (int) ceil( $total / $per_page );
if ( $page > $max_pages && $total > 0 ) {
return new WP_Error(
'rest_search_invalid_page_number',
__( 'The page number requested is larger than the number of pages available.' ),
array( 'status' => 400 )
);
}
$response = rest_ensure_response( $results );
$response->header( 'X-WP-Total', $total );
$response->header( 'X-WP-TotalPages', $max_pages );
$request_params = $request->get_query_params();
$base = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_link = add_query_arg( 'page', $page - 1, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $page < $max_pages ) {
$next_link = add_query_arg( 'page', $page + 1, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepares a single search result for response.
*
* @since 5.0.0
* @since 5.6.0 The `$id` parameter can accept a string.
* @since 5.9.0 Renamed `$id` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param int|string $item ID of the item to prepare.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$item_id = $item;
$handler = $this->get_search_handler( $request );
if ( is_wp_error( $handler ) ) {
return new WP_REST_Response();
}
$fields = $this->get_fields_for_response( $request );
$data = $handler->prepare_item( $item_id, $fields );
$data = $this->add_additional_fields_to_object( $data, $request );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $handler->prepare_item_links( $item_id );
$links['collection'] = array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
);
$response->add_links( $links );
}
return $response;
}
/**
* Retrieves the item schema, conforming to JSON Schema.
*
* @since 5.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$types = array();
$subtypes = array();
foreach ( $this->search_handlers as $search_handler ) {
$types[] = $search_handler->get_type();
$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
}
$types = array_unique( $types );
$subtypes = array_unique( $subtypes );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'search-result',
'type' => 'object',
'properties' => array(
self::PROP_ID => array(
'description' => __( 'Unique identifier for the object.' ),
'type' => array( 'integer', 'string' ),
'context' => array( 'view', 'embed' ),
'readonly' => true,
),
self::PROP_TITLE => array(
'description' => __( 'The title for the object.' ),
'type' => 'string',
'context' => array( 'view', 'embed' ),
'readonly' => true,
),
self::PROP_URL => array(
'description' => __( 'URL to the object.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'embed' ),
'readonly' => true,
),
self::PROP_TYPE => array(
'description' => __( 'Object type.' ),
'type' => 'string',
'enum' => $types,
'context' => array( 'view', 'embed' ),
'readonly' => true,
),
self::PROP_SUBTYPE => array(
'description' => __( 'Object subtype.' ),
'type' => 'string',
'enum' => $subtypes,
'context' => array( 'view', 'embed' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for the search results collection.
*
* @since 5.0.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$types = array();
$subtypes = array();
foreach ( $this->search_handlers as $search_handler ) {
$types[] = $search_handler->get_type();
$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
}
$types = array_unique( $types );
$subtypes = array_unique( $subtypes );
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params[ self::PROP_TYPE ] = array(
'default' => $types[0],
'description' => __( 'Limit results to items of an object type.' ),
'type' => 'string',
'enum' => $types,
);
$query_params[ self::PROP_SUBTYPE ] = array(
'default' => self::TYPE_ANY,
'description' => __( 'Limit results to items of one or more object subtypes.' ),
'type' => 'array',
'items' => array(
'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ),
'type' => 'string',
),
'sanitize_callback' => array( $this, 'sanitize_subtypes' ),
);
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
return $query_params;
}
/**
* Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included.
*
* @since 5.0.0
*
* @param string|array $subtypes One or more subtypes.
* @param WP_REST_Request $request Full details about the request.
* @param string $parameter Parameter name.
* @return string[]|WP_Error List of valid subtypes, or WP_Error object on failure.
*/
public function sanitize_subtypes( $subtypes, $request, $parameter ) {
$subtypes = wp_parse_slug_list( $subtypes );
$subtypes = rest_parse_request_arg( $subtypes, $request, $parameter );
if ( is_wp_error( $subtypes ) ) {
return $subtypes;
}
// 'any' overrides any other subtype.
if ( in_array( self::TYPE_ANY, $subtypes, true ) ) {
return array( self::TYPE_ANY );
}
$handler = $this->get_search_handler( $request );
if ( is_wp_error( $handler ) ) {
return $handler;
}
return array_intersect( $subtypes, $handler->get_subtypes() );
}
/**
* Gets the search handler to handle the current request.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure.
*/
protected function get_search_handler( $request ) {
$type = $request->get_param( self::PROP_TYPE );
if ( ! $type || ! isset( $this->search_handlers[ $type ] ) ) {
return new WP_Error(
'rest_search_invalid_type',
__( 'Invalid type parameter.' ),
array( 'status' => 400 )
);
}
return $this->search_handlers[ $type ];
}
}
endpoints/class-wp-rest-settings-controller.php 0000644 00000024061 15153067731 0016016 0 ustar 00 <?php
/**
* REST API: WP_REST_Settings_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage a site's settings via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Settings_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'settings';
}
/**
* Registers the routes for the site's settings.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'args' => array(),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to read and manage settings.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool True if the request has read access for the item, otherwise false.
*/
public function get_item_permissions_check( $request ) {
return current_user_can( 'manage_options' );
}
/**
* Retrieves the settings.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return array|WP_Error Array on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$options = $this->get_registered_options();
$response = array();
foreach ( $options as $name => $args ) {
/**
* Filters the value of a setting recognized by the REST API.
*
* Allow hijacking the setting value and overriding the built-in behavior by returning a
* non-null value. The returned value will be presented as the setting value instead.
*
* @since 4.7.0
*
* @param mixed $result Value to use for the requested setting. Can be a scalar
* matching the registered schema for the setting, or null to
* follow the default get_option() behavior.
* @param string $name Setting name (as shown in REST API responses).
* @param array $args Arguments passed to register_setting() for this setting.
*/
$response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );
if ( is_null( $response[ $name ] ) ) {
// Default to a null value as "null" in the response means "not set".
$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
}
/*
* Because get_option() is lossy, we have to
* cast values to the type they are registered with.
*/
$response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
}
return $response;
}
/**
* Prepares a value for output based off a schema array.
*
* @since 4.7.0
*
* @param mixed $value Value to prepare.
* @param array $schema Schema to match.
* @return mixed The prepared value.
*/
protected function prepare_value( $value, $schema ) {
/*
* If the value is not valid by the schema, set the value to null.
* Null values are specifically non-destructive, so this will not cause
* overwriting the current invalid value to null.
*/
if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
return null;
}
return rest_sanitize_value_from_schema( $value, $schema );
}
/**
* Updates settings for the settings object.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return array|WP_Error Array on success, or error object on failure.
*/
public function update_item( $request ) {
$options = $this->get_registered_options();
$params = $request->get_params();
foreach ( $options as $name => $args ) {
if ( ! array_key_exists( $name, $params ) ) {
continue;
}
/**
* Filters whether to preempt a setting value update via the REST API.
*
* Allows hijacking the setting update logic and overriding the built-in behavior by
* returning true.
*
* @since 4.7.0
*
* @param bool $result Whether to override the default behavior for updating the
* value of a setting.
* @param string $name Setting name (as shown in REST API responses).
* @param mixed $value Updated setting value.
* @param array $args Arguments passed to register_setting() for this setting.
*/
$updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );
if ( $updated ) {
continue;
}
/*
* A null value for an option would have the same effect as
* deleting the option from the database, and relying on the
* default value.
*/
if ( is_null( $request[ $name ] ) ) {
/*
* A null value is returned in the response for any option
* that has a non-scalar value.
*
* To protect clients from accidentally including the null
* values from a response object in a request, we do not allow
* options with values that don't pass validation to be updated to null.
* Without this added protection a client could mistakenly
* delete all options that have invalid values from the
* database.
*/
if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
return new WP_Error(
'rest_invalid_stored_value',
/* translators: %s: Property name. */
sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
array( 'status' => 500 )
);
}
delete_option( $args['option_name'] );
} else {
update_option( $args['option_name'], $request[ $name ] );
}
}
return $this->get_item( $request );
}
/**
* Retrieves all of the registered options for the Settings API.
*
* @since 4.7.0
*
* @return array Array of registered options.
*/
protected function get_registered_options() {
$rest_options = array();
foreach ( get_registered_settings() as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$defaults = array(
'name' => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
'schema' => array(),
);
$rest_args = array_merge( $defaults, $rest_args );
$default_schema = array(
'type' => empty( $args['type'] ) ? null : $args['type'],
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
$rest_args['option_name'] = $name;
// Skip over settings that don't have a defined type in the schema.
if ( empty( $rest_args['schema']['type'] ) ) {
continue;
}
/*
* Allow the supported types for settings, as we don't want invalid types
* to be updated with arbitrary values that we can't do decent sanitizing for.
*/
if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
continue;
}
$rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] );
$rest_options[ $rest_args['name'] ] = $rest_args;
}
return $rest_options;
}
/**
* Retrieves the site setting schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$options = $this->get_registered_options();
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'settings',
'type' => 'object',
'properties' => array(),
);
foreach ( $options as $option_name => $option ) {
$schema['properties'][ $option_name ] = $option['schema'];
$schema['properties'][ $option_name ]['arg_options'] = array(
'sanitize_callback' => array( $this, 'sanitize_callback' ),
);
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Custom sanitize callback used for all options to allow the use of 'null'.
*
* By default, the schema of settings will throw an error if a value is set to
* `null` as it's not a valid value for something like "type => string". We
* provide a wrapper sanitizer to allow the use of `null`.
*
* @since 4.7.0
*
* @param mixed $value The value for the setting.
* @param WP_REST_Request $request The request object.
* @param string $param The parameter name.
* @return mixed|WP_Error
*/
public function sanitize_callback( $value, $request, $param ) {
if ( is_null( $value ) ) {
return $value;
}
return rest_parse_request_arg( $value, $request, $param );
}
/**
* Recursively add additionalProperties = false to all objects in a schema
* if no additionalProperties setting is specified.
*
* This is needed to restrict properties of objects in settings values to only
* registered items, as the REST API will allow additional properties by
* default.
*
* @since 4.9.0
* @deprecated 6.1.0 Use {@see rest_default_additional_properties_to_false()} instead.
*
* @param array $schema The schema array.
* @return array
*/
protected function set_additional_properties_to_false( $schema ) {
_deprecated_function( __METHOD__, '6.1.0', 'rest_default_additional_properties_to_false()' );
return rest_default_additional_properties_to_false( $schema );
}
}
endpoints/class-wp-rest-sidebars-controller.php 0000644 00000036554 15153067731 0015764 0 ustar 00 <?php
/**
* REST API: WP_REST_Sidebars_Controller class
*
* Original code from {@link https://github.com/martin-pettersson/wp-rest-api-sidebars Martin Pettersson (martin_pettersson@outlook.com)}.
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Core class used to manage a site's sidebars.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Sidebars_Controller extends WP_REST_Controller {
/**
* Tracks whether {@see retrieve_widgets()} has been called in the current request.
*
* @since 5.9.0
* @var bool
*/
protected $widgets_retrieved = false;
/**
* Sidebars controller constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'sidebars';
}
/**
* Registers the controllers routes.
*
* @since 5.8.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'id' => array(
'description' => __( 'The id of a registered sidebar' ),
'type' => 'string',
),
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get sidebars.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$this->retrieve_widgets();
foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
$sidebar = $this->get_sidebar( $id );
if ( ! $sidebar ) {
continue;
}
if ( $this->check_read_permission( $sidebar ) ) {
return true;
}
}
return $this->do_permissions_check();
}
/**
* Retrieves the list of sidebars (active or inactive).
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success.
*/
public function get_items( $request ) {
$this->retrieve_widgets();
$data = array();
$permissions_check = $this->do_permissions_check();
foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
$sidebar = $this->get_sidebar( $id );
if ( ! $sidebar ) {
continue;
}
if ( is_wp_error( $permissions_check ) && ! $this->check_read_permission( $sidebar ) ) {
continue;
}
$data[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response( $sidebar, $request )
);
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to get a single sidebar.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$this->retrieve_widgets();
$sidebar = $this->get_sidebar( $request['id'] );
if ( $sidebar && $this->check_read_permission( $sidebar ) ) {
return true;
}
return $this->do_permissions_check();
}
/**
* Checks if a sidebar can be read publicly.
*
* @since 5.9.0
*
* @param array $sidebar The registered sidebar configuration.
* @return bool Whether the side can be read.
*/
protected function check_read_permission( $sidebar ) {
return ! empty( $sidebar['show_in_rest'] );
}
/**
* Retrieves one sidebar from the collection.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$this->retrieve_widgets();
$sidebar = $this->get_sidebar( $request['id'] );
if ( ! $sidebar ) {
return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $sidebar, $request );
}
/**
* Checks if a given request has access to update sidebars.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
return $this->do_permissions_check();
}
/**
* Updates a sidebar.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
if ( isset( $request['widgets'] ) ) {
$sidebars = wp_get_sidebars_widgets();
foreach ( $sidebars as $sidebar_id => $widgets ) {
foreach ( $widgets as $i => $widget_id ) {
// This automatically removes the passed widget IDs from any other sidebars in use.
if ( $sidebar_id !== $request['id'] && in_array( $widget_id, $request['widgets'], true ) ) {
unset( $sidebars[ $sidebar_id ][ $i ] );
}
// This automatically removes omitted widget IDs to the inactive sidebar.
if ( $sidebar_id === $request['id'] && ! in_array( $widget_id, $request['widgets'], true ) ) {
$sidebars['wp_inactive_widgets'][] = $widget_id;
}
}
}
$sidebars[ $request['id'] ] = $request['widgets'];
wp_set_sidebars_widgets( $sidebars );
}
$request['context'] = 'edit';
$sidebar = $this->get_sidebar( $request['id'] );
/**
* Fires after a sidebar is updated via the REST API.
*
* @since 5.8.0
*
* @param array $sidebar The updated sidebar.
* @param WP_REST_Request $request Request object.
*/
do_action( 'rest_save_sidebar', $sidebar, $request );
return $this->prepare_item_for_response( $sidebar, $request );
}
/**
* Checks if the user has permissions to make the request.
*
* @since 5.8.0
*
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
protected function do_permissions_check() {
/*
* Verify if the current user has edit_theme_options capability.
* This capability is required to access the widgets screen.
*/
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves the registered sidebar with the given id.
*
* @since 5.8.0
*
* @param string|int $id ID of the sidebar.
* @return array|null The discovered sidebar, or null if it is not registered.
*/
protected function get_sidebar( $id ) {
return wp_get_sidebar( $id );
}
/**
* Looks for "lost" widgets once per request.
*
* @since 5.9.0
*
* @see retrieve_widgets()
*/
protected function retrieve_widgets() {
if ( ! $this->widgets_retrieved ) {
retrieve_widgets();
$this->widgets_retrieved = true;
}
}
/**
* Prepares a single sidebar output for response.
*
* @since 5.8.0
* @since 5.9.0 Renamed `$raw_sidebar` to `$item` to match parent class for PHP 8 named parameter support.
*
* @global array $wp_registered_sidebars The registered sidebars.
* @global array $wp_registered_widgets The registered widgets.
*
* @param array $item Sidebar instance.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Prepared response object.
*/
public function prepare_item_for_response( $item, $request ) {
global $wp_registered_sidebars, $wp_registered_widgets;
// Restores the more descriptive, specific name for use within this method.
$raw_sidebar = $item;
$id = $raw_sidebar['id'];
$sidebar = array( 'id' => $id );
if ( isset( $wp_registered_sidebars[ $id ] ) ) {
$registered_sidebar = $wp_registered_sidebars[ $id ];
$sidebar['status'] = 'active';
$sidebar['name'] = isset( $registered_sidebar['name'] ) ? $registered_sidebar['name'] : '';
$sidebar['description'] = isset( $registered_sidebar['description'] ) ? wp_sidebar_description( $id ) : '';
$sidebar['class'] = isset( $registered_sidebar['class'] ) ? $registered_sidebar['class'] : '';
$sidebar['before_widget'] = isset( $registered_sidebar['before_widget'] ) ? $registered_sidebar['before_widget'] : '';
$sidebar['after_widget'] = isset( $registered_sidebar['after_widget'] ) ? $registered_sidebar['after_widget'] : '';
$sidebar['before_title'] = isset( $registered_sidebar['before_title'] ) ? $registered_sidebar['before_title'] : '';
$sidebar['after_title'] = isset( $registered_sidebar['after_title'] ) ? $registered_sidebar['after_title'] : '';
} else {
$sidebar['status'] = 'inactive';
$sidebar['name'] = $raw_sidebar['name'];
$sidebar['description'] = '';
$sidebar['class'] = '';
}
if ( wp_is_block_theme() ) {
$sidebar['status'] = 'inactive';
}
$fields = $this->get_fields_for_response( $request );
if ( rest_is_field_included( 'widgets', $fields ) ) {
$sidebars = wp_get_sidebars_widgets();
$widgets = array_filter(
isset( $sidebars[ $sidebar['id'] ] ) ? $sidebars[ $sidebar['id'] ] : array(),
static function ( $widget_id ) use ( $wp_registered_widgets ) {
return isset( $wp_registered_widgets[ $widget_id ] );
}
);
$sidebar['widgets'] = array_values( $widgets );
}
$schema = $this->get_item_schema();
$data = array();
foreach ( $schema['properties'] as $property_id => $property ) {
if ( isset( $sidebar[ $property_id ] ) && true === rest_validate_value_from_schema( $sidebar[ $property_id ], $property ) ) {
$data[ $property_id ] = $sidebar[ $property_id ];
} elseif ( isset( $property['default'] ) ) {
$data[ $property_id ] = $property['default'];
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $sidebar ) );
}
/**
* Filters the REST API response for a sidebar.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param array $raw_sidebar The raw sidebar data.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_sidebar', $response, $raw_sidebar, $request );
}
/**
* Prepares links for the sidebar.
*
* @since 5.8.0
*
* @param array $sidebar Sidebar.
* @return array Links for the given widget.
*/
protected function prepare_links( $sidebar ) {
return array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $sidebar['id'] ) ),
),
'https://api.w.org/widget' => array(
'href' => add_query_arg( 'sidebar', $sidebar['id'], rest_url( '/wp/v2/widgets' ) ),
'embeddable' => true,
),
);
}
/**
* Retrieves the block type' schema, conforming to JSON Schema.
*
* @since 5.8.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'sidebar',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID of sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Unique name identifying the sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of sidebar.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'class' => array(
'description' => __( 'Extra CSS class to assign to the sidebar in the Widgets interface.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'before_widget' => array(
'description' => __( 'HTML content to prepend to each widget\'s HTML output when assigned to this sidebar. Default is an opening list item element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'after_widget' => array(
'description' => __( 'HTML content to append to each widget\'s HTML output when assigned to this sidebar. Default is a closing list item element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'before_title' => array(
'description' => __( 'HTML content to prepend to the sidebar title when displayed. Default is an opening h2 element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'after_title' => array(
'description' => __( 'HTML content to append to the sidebar title when displayed. Default is a closing h2 element.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Status of sidebar.' ),
'type' => 'string',
'enum' => array( 'active', 'inactive' ),
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'widgets' => array(
'description' => __( 'Nested widgets.' ),
'type' => 'array',
'items' => array(
'type' => array( 'object', 'string' ),
),
'default' => array(),
'context' => array( 'embed', 'view', 'edit' ),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-site-health-controller.php 0000644 00000023154 15153067731 0016367 0 ustar 00 <?php
/**
* REST API: WP_REST_Site_Health_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.6.0
*/
/**
* Core class for interacting with Site Health tests.
*
* @since 5.6.0
*
* @see WP_REST_Controller
*/
class WP_REST_Site_Health_Controller extends WP_REST_Controller {
/**
* An instance of the site health class.
*
* @since 5.6.0
*
* @var WP_Site_Health
*/
private $site_health;
/**
* Site Health controller constructor.
*
* @since 5.6.0
*
* @param WP_Site_Health $site_health An instance of the site health class.
*/
public function __construct( $site_health ) {
$this->namespace = 'wp-site-health/v1';
$this->rest_base = 'tests';
$this->site_health = $site_health;
}
/**
* Registers API routes.
*
* @since 5.6.0
* @since 6.1.0 Adds page-cache async test.
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'background-updates'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_background_updates' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'background_updates' );
},
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'loopback-requests'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_loopback_requests' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'loopback_requests' );
},
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'https-status'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_https_status' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'https_status' );
},
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'dotorg-communication'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_dotorg_communication' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'dotorg_communication' );
},
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'authorization-header'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_authorization_header' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'authorization_header' );
},
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s',
'directory-sizes'
),
array(
'methods' => 'GET',
'callback' => array( $this, 'get_directory_sizes' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'directory_sizes' ) && ! is_multisite();
},
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/%s',
$this->rest_base,
'page-cache'
),
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'test_page_cache' ),
'permission_callback' => function () {
return $this->validate_request_permission( 'page_cache' );
},
),
)
);
}
/**
* Validates if the current user can request this REST endpoint.
*
* @since 5.6.0
*
* @param string $check The endpoint check being ran.
* @return bool
*/
protected function validate_request_permission( $check ) {
$default_capability = 'view_site_health_checks';
/**
* Filters the capability needed to run a given Site Health check.
*
* @since 5.6.0
*
* @param string $default_capability The default capability required for this check.
* @param string $check The Site Health check being performed.
*/
$capability = apply_filters( "site_health_test_rest_capability_{$check}", $default_capability, $check );
return current_user_can( $capability );
}
/**
* Checks if background updates work as expected.
*
* @since 5.6.0
*
* @return array
*/
public function test_background_updates() {
$this->load_admin_textdomain();
return $this->site_health->get_test_background_updates();
}
/**
* Checks that the site can reach the WordPress.org API.
*
* @since 5.6.0
*
* @return array
*/
public function test_dotorg_communication() {
$this->load_admin_textdomain();
return $this->site_health->get_test_dotorg_communication();
}
/**
* Checks that loopbacks can be performed.
*
* @since 5.6.0
*
* @return array
*/
public function test_loopback_requests() {
$this->load_admin_textdomain();
return $this->site_health->get_test_loopback_requests();
}
/**
* Checks that the site's frontend can be accessed over HTTPS.
*
* @since 5.7.0
*
* @return array
*/
public function test_https_status() {
$this->load_admin_textdomain();
return $this->site_health->get_test_https_status();
}
/**
* Checks that the authorization header is valid.
*
* @since 5.6.0
*
* @return array
*/
public function test_authorization_header() {
$this->load_admin_textdomain();
return $this->site_health->get_test_authorization_header();
}
/**
* Checks that full page cache is active.
*
* @since 6.1.0
*
* @return array The test result.
*/
public function test_page_cache() {
$this->load_admin_textdomain();
return $this->site_health->get_test_page_cache();
}
/**
* Gets the current directory sizes for this install.
*
* @since 5.6.0
*
* @return array|WP_Error
*/
public function get_directory_sizes() {
if ( ! class_exists( 'WP_Debug_Data' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
}
$this->load_admin_textdomain();
$sizes_data = WP_Debug_Data::get_sizes();
$all_sizes = array( 'raw' => 0 );
foreach ( $sizes_data as $name => $value ) {
$name = sanitize_text_field( $name );
$data = array();
if ( isset( $value['size'] ) ) {
if ( is_string( $value['size'] ) ) {
$data['size'] = sanitize_text_field( $value['size'] );
} else {
$data['size'] = (int) $value['size'];
}
}
if ( isset( $value['debug'] ) ) {
if ( is_string( $value['debug'] ) ) {
$data['debug'] = sanitize_text_field( $value['debug'] );
} else {
$data['debug'] = (int) $value['debug'];
}
}
if ( ! empty( $value['raw'] ) ) {
$data['raw'] = (int) $value['raw'];
}
$all_sizes[ $name ] = $data;
}
if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) {
return new WP_Error( 'not_available', __( 'Directory sizes could not be returned.' ), array( 'status' => 500 ) );
}
return $all_sizes;
}
/**
* Loads the admin textdomain for Site Health tests.
*
* The {@see WP_Site_Health} class is defined in WP-Admin, while the REST API operates in a front-end context.
* This means that the translations for Site Health won't be loaded by default in {@see load_default_textdomain()}.
*
* @since 5.6.0
*/
protected function load_admin_textdomain() {
// Accounts for inner REST API requests in the admin.
if ( ! is_admin() ) {
$locale = determine_locale();
load_textdomain( 'default', WP_LANG_DIR . "/admin-$locale.mo", $locale );
}
}
/**
* Gets the schema for each site health test.
*
* @since 5.6.0
*
* @return array The test schema.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->schema;
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'wp-site-health-test',
'type' => 'object',
'properties' => array(
'test' => array(
'type' => 'string',
'description' => __( 'The name of the test being run.' ),
'readonly' => true,
),
'label' => array(
'type' => 'string',
'description' => __( 'A label describing the test.' ),
'readonly' => true,
),
'status' => array(
'type' => 'string',
'description' => __( 'The status of the test.' ),
'enum' => array( 'good', 'recommended', 'critical' ),
'readonly' => true,
),
'badge' => array(
'type' => 'object',
'description' => __( 'The category this test is grouped in.' ),
'properties' => array(
'label' => array(
'type' => 'string',
'readonly' => true,
),
'color' => array(
'type' => 'string',
'enum' => array( 'blue', 'orange', 'red', 'green', 'purple', 'gray' ),
'readonly' => true,
),
),
'readonly' => true,
),
'description' => array(
'type' => 'string',
'description' => __( 'A more descriptive explanation of what the test looks for, and why it is important for the user.' ),
'readonly' => true,
),
'actions' => array(
'type' => 'string',
'description' => __( 'HTML containing an action to direct the user to where they can resolve the issue.' ),
'readonly' => true,
),
),
);
return $this->schema;
}
}
endpoints/class-wp-rest-taxonomies-controller.php 0000644 00000032344 15153067731 0016347 0 ustar 00 <?php
/**
* REST API: WP_REST_Taxonomies_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage taxonomies via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Taxonomies_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'taxonomies';
}
/**
* Registers the routes for taxonomies.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<taxonomy>[\w-]+)',
array(
'args' => array(
'taxonomy' => array(
'description' => __( 'An alphanumeric identifier for the taxonomy.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks whether a given request has permission to read taxonomies.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( 'edit' === $request['context'] ) {
if ( ! empty( $request['type'] ) ) {
$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
} else {
$taxonomies = get_taxonomies( '', 'objects' );
}
foreach ( $taxonomies as $taxonomy ) {
if ( ! empty( $taxonomy->show_in_rest ) && current_user_can( $taxonomy->cap->assign_terms ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to manage terms in this taxonomy.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves all public taxonomies.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
if ( isset( $registered['type'] ) && ! empty( $request['type'] ) ) {
$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
} else {
$taxonomies = get_taxonomies( '', 'objects' );
}
$data = array();
foreach ( $taxonomies as $tax_type => $value ) {
if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->assign_terms ) ) ) {
continue;
}
$tax = $this->prepare_item_for_response( $value, $request );
$tax = $this->prepare_response_for_collection( $tax );
$data[ $tax_type ] = $tax;
}
if ( empty( $data ) ) {
// Response should still be returned as a JSON object when it is empty.
$data = (object) $data;
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise false or WP_Error object.
*/
public function get_item_permissions_check( $request ) {
$tax_obj = get_taxonomy( $request['taxonomy'] );
if ( $tax_obj ) {
if ( empty( $tax_obj->show_in_rest ) ) {
return false;
}
if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->assign_terms ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to manage terms in this taxonomy.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
return true;
}
/**
* Retrieves a specific taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$tax_obj = get_taxonomy( $request['taxonomy'] );
if ( empty( $tax_obj ) ) {
return new WP_Error(
'rest_taxonomy_invalid',
__( 'Invalid taxonomy.' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $tax_obj, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a taxonomy object for serialization.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$taxonomy` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Taxonomy $item Taxonomy data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$taxonomy = $item;
$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'name', $fields, true ) ) {
$data['name'] = $taxonomy->label;
}
if ( in_array( 'slug', $fields, true ) ) {
$data['slug'] = $taxonomy->name;
}
if ( in_array( 'capabilities', $fields, true ) ) {
$data['capabilities'] = $taxonomy->cap;
}
if ( in_array( 'description', $fields, true ) ) {
$data['description'] = $taxonomy->description;
}
if ( in_array( 'labels', $fields, true ) ) {
$data['labels'] = $taxonomy->labels;
}
if ( in_array( 'types', $fields, true ) ) {
$data['types'] = array_values( $taxonomy->object_type );
}
if ( in_array( 'show_cloud', $fields, true ) ) {
$data['show_cloud'] = $taxonomy->show_tagcloud;
}
if ( in_array( 'hierarchical', $fields, true ) ) {
$data['hierarchical'] = $taxonomy->hierarchical;
}
if ( in_array( 'rest_base', $fields, true ) ) {
$data['rest_base'] = $base;
}
if ( in_array( 'rest_namespace', $fields, true ) ) {
$data['rest_namespace'] = $taxonomy->rest_namespace;
}
if ( in_array( 'visibility', $fields, true ) ) {
$data['visibility'] = array(
'public' => (bool) $taxonomy->public,
'publicly_queryable' => (bool) $taxonomy->publicly_queryable,
'show_admin_column' => (bool) $taxonomy->show_admin_column,
'show_in_nav_menus' => (bool) $taxonomy->show_in_nav_menus,
'show_in_quick_edit' => (bool) $taxonomy->show_in_quick_edit,
'show_ui' => (bool) $taxonomy->show_ui,
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $taxonomy ) );
}
/**
* Filters a taxonomy returned from the REST API.
*
* Allows modification of the taxonomy data right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Taxonomy $item The original taxonomy object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
}
/**
* Prepares links for the request.
*
* @since 6.1.0
*
* @param WP_Taxonomy $taxonomy The taxonomy.
* @return array Links for the given taxonomy.
*/
protected function prepare_links( $taxonomy ) {
return array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'https://api.w.org/items' => array(
'href' => rest_url( rest_get_route_for_taxonomy_items( $taxonomy->name ) ),
),
);
}
/**
* Retrieves the taxonomy's schema, conforming to JSON Schema.
*
* @since 4.7.0
* @since 5.0.0 The `visibility` property was added.
* @since 5.9.0 The `rest_namespace` property was added.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'taxonomy',
'type' => 'object',
'properties' => array(
'capabilities' => array(
'description' => __( 'All capabilities used by the taxonomy.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the taxonomy.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hierarchical' => array(
'description' => __( 'Whether or not the taxonomy should have children.' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'labels' => array(
'description' => __( 'Human-readable labels for the taxonomy for various contexts.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'The title for the taxonomy.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the taxonomy.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'show_cloud' => array(
'description' => __( 'Whether or not the term cloud should be displayed.' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'readonly' => true,
),
'types' => array(
'description' => __( 'Types associated with the taxonomy.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rest_base' => array(
'description' => __( 'REST base route for the taxonomy.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'rest_namespace' => array(
'description' => __( 'REST namespace route for the taxonomy.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'visibility' => array(
'description' => __( 'The visibility settings for the taxonomy.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
'properties' => array(
'public' => array(
'description' => __( 'Whether a taxonomy is intended for use publicly either via the admin interface or by front-end users.' ),
'type' => 'boolean',
),
'publicly_queryable' => array(
'description' => __( 'Whether the taxonomy is publicly queryable.' ),
'type' => 'boolean',
),
'show_ui' => array(
'description' => __( 'Whether to generate a default UI for managing this taxonomy.' ),
'type' => 'boolean',
),
'show_admin_column' => array(
'description' => __( 'Whether to allow automatic creation of taxonomy columns on associated post-types table.' ),
'type' => 'boolean',
),
'show_in_nav_menus' => array(
'description' => __( 'Whether to make the taxonomy available for selection in navigation menus.' ),
'type' => 'boolean',
),
'show_in_quick_edit' => array(
'description' => __( 'Whether to show the taxonomy in the quick/bulk edit panel.' ),
'type' => 'boolean',
),
),
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$new_params = array();
$new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$new_params['type'] = array(
'description' => __( 'Limit results to taxonomies associated with a specific post type.' ),
'type' => 'string',
);
return $new_params;
}
}
endpoints/class-wp-rest-template-autosaves-controller.php 0000644 00000017026 15153067731 0020004 0 ustar 00 <?php
/**
* REST API: WP_REST_Template_Autosaves_Controller class.
*
* @package WordPress
* @subpackage REST_API
* @since 6.4.0
*/
/**
* Core class used to access template autosaves via the REST API.
*
* @since 6.4.0
*
* @see WP_REST_Autosaves_Controller
*/
class WP_REST_Template_Autosaves_Controller extends WP_REST_Autosaves_Controller {
/**
* Parent post type.
*
* @since 6.4.0
* @var string
*/
private $parent_post_type;
/**
* Parent post controller.
*
* @since 6.4.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* Revision controller.
*
* @since 6.4.0
* @var WP_REST_Revisions_Controller
*/
private $revisions_controller;
/**
* The base of the parent controller's route.
*
* @since 6.4.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 6.4.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
parent::__construct( $parent_post_type );
$this->parent_post_type = $parent_post_type;
$post_type_object = get_post_type_object( $parent_post_type );
$parent_controller = $post_type_object->get_rest_controller();
if ( ! $parent_controller ) {
$parent_controller = new WP_REST_Templates_Controller( $parent_post_type );
}
$this->parent_controller = $parent_controller;
$revisions_controller = $post_type_object->get_revisions_rest_controller();
if ( ! $revisions_controller ) {
$revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
}
$this->revisions_controller = $revisions_controller;
$this->rest_base = 'autosaves';
$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
$this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
}
/**
* Registers the routes for autosaves.
*
* @since 6.4.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
sprintf(
'/%s/(?P<id>%s%s)/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base
),
array(
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/(?P<parent>%s%s)/%s/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base,
'(?P<id>[\d]+)'
),
array(
'args' => array(
'parent' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
),
'id' => array(
'description' => __( 'The ID for the autosave.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Prepares the item for the REST response.
*
* @since 6.4.0
*
* @param WP_Post $item Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
$template = _build_block_template_result_from_post( $item );
$response = $this->parent_controller->prepare_item_for_response( $template, $request );
$fields = $this->get_fields_for_response( $request );
$data = $response->get_data();
if ( in_array( 'parent', $fields, true ) ) {
$data['parent'] = (int) $item->post_parent;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = new WP_REST_Response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $template );
$response->add_links( $links );
}
return $response;
}
/**
* Gets the autosave, if the ID is valid.
*
* @since 6.4.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Post|WP_Error Autosave post object if ID is valid, WP_Error otherwise.
*/
public function get_item( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
$autosave = wp_get_post_autosave( $parent->ID );
if ( ! $autosave ) {
return new WP_Error(
'rest_post_no_autosave',
__( 'There is no autosave revision for this template.' ),
array( 'status' => 404 )
);
}
$response = $this->prepare_item_for_response( $autosave, $request );
return $response;
}
/**
* Get the parent post.
*
* @since 6.4.0
*
* @param int $parent_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent( $parent_id ) {
return $this->revisions_controller->get_parent( $parent_id );
}
/**
* Prepares links for the request.
*
* @since 6.4.0
*
* @param WP_Block_Template $template Template.
* @return array Links for the given post.
*/
protected function prepare_links( $template ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ),
),
'parent' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ),
),
);
return $links;
}
/**
* Retrieves the autosave's schema, conforming to JSON Schema.
*
* @since 6.4.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = $this->revisions_controller->get_item_schema();
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-template-revisions-controller.php 0000644 00000020124 15153067731 0020004 0 ustar 00 <?php
/**
* REST API: WP_REST_Template_Revisions_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 6.4.0
*/
/**
* Core class used to access template revisions via the REST API.
*
* @since 6.4.0
*
* @see WP_REST_Controller
*/
class WP_REST_Template_Revisions_Controller extends WP_REST_Revisions_Controller {
/**
* Parent post type.
*
* @since 6.4.0
* @var string
*/
private $parent_post_type;
/**
* Parent controller.
*
* @since 6.4.0
* @var WP_REST_Controller
*/
private $parent_controller;
/**
* The base of the parent controller's route.
*
* @since 6.4.0
* @var string
*/
private $parent_base;
/**
* Constructor.
*
* @since 6.4.0
*
* @param string $parent_post_type Post type of the parent.
*/
public function __construct( $parent_post_type ) {
parent::__construct( $parent_post_type );
$this->parent_post_type = $parent_post_type;
$post_type_object = get_post_type_object( $parent_post_type );
$parent_controller = $post_type_object->get_rest_controller();
if ( ! $parent_controller ) {
$parent_controller = new WP_REST_Templates_Controller( $parent_post_type );
}
$this->parent_controller = $parent_controller;
$this->rest_base = 'revisions';
$this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
$this->namespace = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
}
/**
* Registers the routes for revisions based on post types supporting revisions.
*
* @since 6.4.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
sprintf(
'/%s/(?P<parent>%s%s)/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base
),
array(
'args' => array(
'parent' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf(
'/%s/(?P<parent>%s%s)/%s/%s',
$this->parent_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+',
$this->rest_base,
'(?P<id>[\d]+)'
),
array(
'args' => array(
'parent' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
),
'id' => array(
'description' => __( 'Unique identifier for the revision.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Required to be true, as revisions do not support trashing.' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Gets the parent post, if the ID is valid.
*
* @since 6.4.0
*
* @param int $parent_post_id Supplied ID.
* @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
*/
protected function get_parent( $parent_post_id ) {
$template = get_block_template( $parent_post_id, $this->parent_post_type );
if ( ! $template ) {
return new WP_Error(
'rest_post_invalid_parent',
__( 'Invalid template parent ID.' ),
array( 'status' => 404 )
);
}
return get_post( $template->wp_id );
}
/**
* Prepares the item for the REST response.
*
* @since 6.4.0
*
* @param WP_Post $item Post revision object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
$template = _build_block_template_result_from_post( $item );
$response = $this->parent_controller->prepare_item_for_response( $template, $request );
$fields = $this->get_fields_for_response( $request );
$data = $response->get_data();
if ( in_array( 'parent', $fields, true ) ) {
$data['parent'] = (int) $item->post_parent;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = new WP_REST_Response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $template );
$response->add_links( $links );
}
return $response;
}
/**
* Checks if a given request has access to delete a revision.
*
* @since 6.4.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$parent = $this->get_parent( $request['parent'] );
if ( is_wp_error( $parent ) ) {
return $parent;
}
if ( ! current_user_can( 'delete_post', $parent->ID ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete revisions of this post.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) {
return $revision;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this revision.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Prepares links for the request.
*
* @since 6.4.0
*
* @param WP_Block_Template $template Template.
* @return array Links for the given post.
*/
protected function prepare_links( $template ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ),
),
'parent' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ),
),
);
return $links;
}
/**
* Retrieves the item's schema, conforming to JSON Schema.
*
* @since 6.4.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = $this->parent_controller->get_item_schema();
$schema['properties']['parent'] = array(
'description' => __( 'The ID for the parent of the revision.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-templates-controller.php 0000644 00000104375 15153067731 0016163 0 ustar 00 <?php
/**
* REST API: WP_REST_Templates_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Base Templates REST API Controller.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Templates_Controller extends WP_REST_Controller {
/**
* Post type.
*
* @since 5.8.0
* @var string
*/
protected $post_type;
/**
* Constructor.
*
* @since 5.8.0
*
* @param string $post_type Post type.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
}
/**
* Registers the controllers routes.
*
* @since 5.8.0
* @since 6.1.0 Endpoint for fallback template content.
*/
public function register_routes() {
// Lists all templates.
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
// Get fallback template content.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/lookup',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_template_fallback' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'slug' => array(
'description' => __( 'The slug of the template to get the fallback for' ),
'type' => 'string',
'required' => true,
),
'is_custom' => array(
'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ),
'type' => 'boolean',
),
'template_prefix' => array(
'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ),
'type' => 'string',
),
),
),
)
);
// Lists/updates a single template based on the given id.
register_rest_route(
$this->namespace,
// The route.
sprintf(
'/%s/(?P<id>%s%s)',
$this->rest_base,
/*
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
// Matches the template name.
'[\/\w%-]+'
),
array(
'args' => array(
'id' => array(
'description' => __( 'The id of a template' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_template_id' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Whether to bypass Trash and force deletion.' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Returns the fallback template for the given slug.
*
* @since 6.1.0
* @since 6.3.0 Ignore empty templates.
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
*/
public function get_template_fallback( $request ) {
$hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );
do {
$fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' );
array_shift( $hierarchy );
} while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) );
// To maintain original behavior, return an empty object rather than a 404 error when no template is found.
$response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass();
return rest_ensure_response( $response );
}
/**
* Checks if the user has permissions to make the request.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
protected function permissions_check( $request ) {
/*
* Verify if the current user has edit_theme_options capability.
* This capability is required to edit/view/delete templates.
*/
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Requesting this endpoint for a template like 'twentytwentytwo//home'
* requires using a path like /wp/v2/templates/twentytwentytwo//home. There
* are special cases when WordPress routing corrects the name to contain
* only a single slash like 'twentytwentytwo/home'.
*
* This method doubles the last slash if it's not already doubled. It relies
* on the template ID format {theme_name}//{template_slug} and the fact that
* slugs cannot contain slashes.
*
* @since 5.9.0
* @see https://core.trac.wordpress.org/ticket/54507
*
* @param string $id Template ID.
* @return string Sanitized template ID.
*/
public function _sanitize_template_id( $id ) {
$id = urldecode( $id );
$last_slash_pos = strrpos( $id, '/' );
if ( false === $last_slash_pos ) {
return $id;
}
$is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
if ( $is_double_slashed ) {
return $id;
}
return (
substr( $id, 0, $last_slash_pos )
. '/'
. substr( $id, $last_slash_pos )
);
}
/**
* Checks if a given request has access to read templates.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Returns a list of templates.
*
* @since 5.8.0
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response
*/
public function get_items( $request ) {
$query = array();
if ( isset( $request['wp_id'] ) ) {
$query['wp_id'] = $request['wp_id'];
}
if ( isset( $request['area'] ) ) {
$query['area'] = $request['area'];
}
if ( isset( $request['post_type'] ) ) {
$query['post_type'] = $request['post_type'];
}
$templates = array();
foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
$data = $this->prepare_item_for_response( $template, $request );
$templates[] = $this->prepare_response_for_collection( $data );
}
return rest_ensure_response( $templates );
}
/**
* Checks if a given request has access to read a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Returns the given template
*
* @since 5.8.0
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
$template = get_block_file_template( $request['id'], $this->post_type );
} else {
$template = get_block_template( $request['id'], $this->post_type );
}
if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $template, $request );
}
/**
* Checks if a given request has access to write a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Updates a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$template = get_block_template( $request['id'], $this->post_type );
if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}
$post_before = get_post( $template->wp_id );
if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
wp_delete_post( $template->wp_id, true );
$request->set_param( 'context', 'edit' );
$template = get_block_template( $request['id'], $this->post_type );
$response = $this->prepare_item_for_response( $template, $request );
return rest_ensure_response( $response );
}
$changes = $this->prepare_item_for_database( $request );
if ( is_wp_error( $changes ) ) {
return $changes;
}
if ( 'custom' === $template->source ) {
$update = true;
$result = wp_update_post( wp_slash( (array) $changes ), false );
} else {
$update = false;
$post_before = null;
$result = wp_insert_post( wp_slash( (array) $changes ), false );
}
if ( is_wp_error( $result ) ) {
if ( 'db_update_error' === $result->get_error_code() ) {
$result->add_data( array( 'status' => 500 ) );
} else {
$result->add_data( array( 'status' => 400 ) );
}
return $result;
}
$template = get_block_template( $request['id'], $this->post_type );
$fields_update = $this->update_additional_fields_for_object( $template, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
$post = get_post( $template->wp_id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );
wp_after_insert_post( $post, $update, $post_before );
$response = $this->prepare_item_for_response( $template, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a given request has access to create a template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Creates a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$prepared_post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_post ) ) {
return $prepared_post;
}
$prepared_post->post_name = $request['slug'];
$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true );
if ( is_wp_error( $post_id ) ) {
if ( 'db_insert_error' === $post_id->get_error_code() ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type );
if ( ! count( $posts ) ) {
return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) );
}
$id = $posts[0]->id;
$post = get_post( $post_id );
$template = get_block_template( $id, $this->post_type );
$fields_update = $this->update_additional_fields_for_object( $template, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );
wp_after_insert_post( $post, false, null );
$response = $this->prepare_item_for_response( $template, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) );
return $response;
}
/**
* Checks if a given request has access to delete a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Deletes a single template.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$template = get_block_template( $request['id'], $this->post_type );
if ( ! $template ) {
return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
}
if ( 'custom' !== $template->source ) {
return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) );
}
$id = $template->wp_id;
$force = (bool) $request['force'];
$request->set_param( 'context', 'edit' );
// If we're forcing, then delete permanently.
if ( $force ) {
$previous = $this->prepare_item_for_response( $template, $request );
$result = wp_delete_post( $id, true );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
} else {
// Otherwise, only trash if we haven't already.
if ( 'trash' === $template->status ) {
return new WP_Error(
'rest_template_already_trashed',
__( 'The template has already been deleted.' ),
array( 'status' => 410 )
);
}
/*
* (Note that internally this falls through to `wp_delete_post()`
* if the Trash is disabled.)
*/
$result = wp_trash_post( $id );
$template->status = 'trash';
$response = $this->prepare_item_for_response( $template, $request );
}
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The template cannot be deleted.' ),
array( 'status' => 500 )
);
}
return $response;
}
/**
* Prepares a single template for create or update.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Changes to pass to wp_update_post.
*/
protected function prepare_item_for_database( $request ) {
$template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
$changes = new stdClass();
if ( null === $template ) {
$changes->post_type = $this->post_type;
$changes->post_status = 'publish';
$changes->tax_input = array(
'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
);
} elseif ( 'custom' !== $template->source ) {
$changes->post_name = $template->slug;
$changes->post_type = $this->post_type;
$changes->post_status = 'publish';
$changes->tax_input = array(
'wp_theme' => $template->theme,
);
$changes->meta_input = array(
'origin' => $template->source,
);
} else {
$changes->post_name = $template->slug;
$changes->ID = $template->wp_id;
$changes->post_status = 'publish';
}
if ( isset( $request['content'] ) ) {
if ( is_string( $request['content'] ) ) {
$changes->post_content = $request['content'];
} elseif ( isset( $request['content']['raw'] ) ) {
$changes->post_content = $request['content']['raw'];
}
} elseif ( null !== $template && 'custom' !== $template->source ) {
$changes->post_content = $template->content;
}
if ( isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$changes->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$changes->post_title = $request['title']['raw'];
}
} elseif ( null !== $template && 'custom' !== $template->source ) {
$changes->post_title = $template->title;
}
if ( isset( $request['description'] ) ) {
$changes->post_excerpt = $request['description'];
} elseif ( null !== $template && 'custom' !== $template->source ) {
$changes->post_excerpt = $template->description;
}
if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) {
$changes->meta_input = wp_parse_args(
array(
'is_wp_suggestion' => $request['is_wp_suggestion'],
),
$changes->meta_input = array()
);
}
if ( 'wp_template_part' === $this->post_type ) {
if ( isset( $request['area'] ) ) {
$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
} elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
} elseif ( empty( $template->area ) ) {
$changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
}
}
if ( ! empty( $request['author'] ) ) {
$post_author = (int) $request['author'];
if ( get_current_user_id() !== $post_author ) {
$user_obj = get_userdata( $post_author );
if ( ! $user_obj ) {
return new WP_Error(
'rest_invalid_author',
__( 'Invalid author ID.' ),
array( 'status' => 400 )
);
}
}
$changes->post_author = $post_author;
}
/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request );
}
/**
* Prepare a single template output for response
*
* @since 5.8.0
* @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support.
* @since 6.3.0 Added `modified` property to the response.
*
* @param WP_Block_Template $item Template instance.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$template = $item;
$fields = $this->get_fields_for_response( $request );
// Base fields for every template.
$data = array();
if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $template->id;
}
if ( rest_is_field_included( 'theme', $fields ) ) {
$data['theme'] = $template->theme;
}
if ( rest_is_field_included( 'content', $fields ) ) {
$data['content'] = array();
}
if ( rest_is_field_included( 'content.raw', $fields ) ) {
$data['content']['raw'] = $template->content;
}
if ( rest_is_field_included( 'content.block_version', $fields ) ) {
$data['content']['block_version'] = block_version( $template->content );
}
if ( rest_is_field_included( 'slug', $fields ) ) {
$data['slug'] = $template->slug;
}
if ( rest_is_field_included( 'source', $fields ) ) {
$data['source'] = $template->source;
}
if ( rest_is_field_included( 'origin', $fields ) ) {
$data['origin'] = $template->origin;
}
if ( rest_is_field_included( 'type', $fields ) ) {
$data['type'] = $template->type;
}
if ( rest_is_field_included( 'description', $fields ) ) {
$data['description'] = $template->description;
}
if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $template->title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
if ( $template->wp_id ) {
/** This filter is documented in wp-includes/post-template.php */
$data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id );
} else {
$data['title']['rendered'] = $template->title;
}
}
if ( rest_is_field_included( 'status', $fields ) ) {
$data['status'] = $template->status;
}
if ( rest_is_field_included( 'wp_id', $fields ) ) {
$data['wp_id'] = (int) $template->wp_id;
}
if ( rest_is_field_included( 'has_theme_file', $fields ) ) {
$data['has_theme_file'] = (bool) $template->has_theme_file;
}
if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) {
$data['is_custom'] = $template->is_custom;
}
if ( rest_is_field_included( 'author', $fields ) ) {
$data['author'] = (int) $template->author;
}
if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) {
$data['area'] = $template->area;
}
if ( rest_is_field_included( 'modified', $fields ) ) {
$data['modified'] = mysql_to_rfc3339( $template->modified );
}
if ( rest_is_field_included( 'author_text', $fields ) ) {
$data['author_text'] = self::get_wp_templates_author_text_field( $template );
}
if ( rest_is_field_included( 'original_source', $fields ) ) {
$data['original_source'] = self::get_wp_templates_original_source_field( $template );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$links = $this->prepare_links( $template->id );
$response->add_links( $links );
if ( ! empty( $links['self']['href'] ) ) {
$actions = $this->get_available_actions();
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}
return $response;
}
/**
* Returns the source from where the template originally comes from.
*
* @since 6.5.0
*
* @param WP_Block_Template $template_object Template instance.
* @return string Original source of the template one of theme, plugin, site, or user.
*/
private static function get_wp_templates_original_source_field( $template_object ) {
if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) {
// Added by theme.
// Template originally provided by a theme, but customized by a user.
// Templates originally didn't have the 'origin' field so identify
// older customized templates by checking for no origin and a 'theme'
// or 'custom' source.
if ( $template_object->has_theme_file &&
( 'theme' === $template_object->origin || (
empty( $template_object->origin ) && in_array(
$template_object->source,
array(
'theme',
'custom',
),
true
) )
)
) {
return 'theme';
}
// Added by plugin.
if ( $template_object->has_theme_file && 'plugin' === $template_object->origin ) {
return 'plugin';
}
// Added by site.
// Template was created from scratch, but has no author. Author support
// was only added to templates in WordPress 5.9. Fallback to showing the
// site logo and title.
if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) {
return 'site';
}
}
// Added by user.
return 'user';
}
/**
* Returns a human readable text for the author of the template.
*
* @since 6.5.0
*
* @param WP_Block_Template $template_object Template instance.
* @return string Human readable text for the author.
*/
private static function get_wp_templates_author_text_field( $template_object ) {
$original_source = self::get_wp_templates_original_source_field( $template_object );
switch ( $original_source ) {
case 'theme':
$theme_name = wp_get_theme( $template_object->theme )->get( 'Name' );
return empty( $theme_name ) ? $template_object->theme : $theme_name;
case 'plugin':
$plugins = get_plugins();
$plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ) ];
return empty( $plugin['Name'] ) ? $template_object->theme : $plugin['Name'];
case 'site':
return get_bloginfo( 'name' );
case 'user':
$author = get_user_by( 'id', $template_object->author );
if ( ! $author ) {
return __( 'Unknown author' );
}
return $author->get( 'display_name' );
}
}
/**
* Prepares links for the request.
*
* @since 5.8.0
*
* @param integer $id ID.
* @return array Links for the given post.
*/
protected function prepare_links( $id ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ),
),
'collection' => array(
'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
),
'about' => array(
'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
),
);
if ( post_type_supports( $this->post_type, 'revisions' ) ) {
$template = get_block_template( $id, $this->post_type );
if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) {
$revisions = wp_get_latest_revision_id_and_total_count( $template->wp_id );
$revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
$revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id );
$links['version-history'] = array(
'href' => rest_url( $revisions_base ),
'count' => $revisions_count,
);
if ( $revisions_count > 0 ) {
$links['predecessor-version'] = array(
'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ),
'id' => $revisions['latest_id'],
);
}
}
}
return $links;
}
/**
* Get the link relations available for the post and current user.
*
* @since 5.8.0
*
* @return string[] List of link relations.
*/
protected function get_available_actions() {
$rels = array();
$post_type = get_post_type_object( $this->post_type );
if ( current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-publish';
}
if ( current_user_can( 'unfiltered_html' ) ) {
$rels[] = 'https://api.w.org/action-unfiltered-html';
}
return $rels;
}
/**
* Retrieves the query params for the posts collection.
*
* @since 5.8.0
* @since 5.9.0 Added `'area'` and `'post_type'`.
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'wp_id' => array(
'description' => __( 'Limit to the specified post id.' ),
'type' => 'integer',
),
'area' => array(
'description' => __( 'Limit to the specified template part area.' ),
'type' => 'string',
),
'post_type' => array(
'description' => __( 'Post type to get the templates for.' ),
'type' => 'string',
),
);
}
/**
* Retrieves the block type' schema, conforming to JSON Schema.
*
* @since 5.8.0
* @since 5.9.0 Added `'area'`.
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID of template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Unique slug identifying the template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'required' => true,
'minLength' => 1,
'pattern' => '[a-zA-Z0-9_\%-]+',
),
'theme' => array(
'description' => __( 'Theme identifier for the template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
'type' => array(
'description' => __( 'Type of template.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
'source' => array(
'description' => __( 'Source of template' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'origin' => array(
'description' => __( 'Source of a customized template' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'content' => array(
'description' => __( 'Content of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'properties' => array(
'raw' => array(
'description' => __( 'Content for the template, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'block_version' => array(
'description' => __( 'Version of the content block format used by the template.' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
),
),
),
'title' => array(
'description' => __( 'Title of template.' ),
'type' => array( 'object', 'string' ),
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the template, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'rendered' => array(
'description' => __( 'HTML title for the template, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
),
'description' => array(
'description' => __( 'Description of template.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Status of template.' ),
'type' => 'string',
'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ),
'default' => 'publish',
'context' => array( 'embed', 'view', 'edit' ),
),
'wp_id' => array(
'description' => __( 'Post ID.' ),
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'has_theme_file' => array(
'description' => __( 'Theme file exists.' ),
'type' => 'bool',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'author' => array(
'description' => __( 'The ID for the author of the template.' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
),
'modified' => array(
'description' => __( "The date the template was last modified, in the site's timezone." ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'author_text' => array(
'type' => 'string',
'description' => __( 'Human readable text for the author.' ),
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'original_source' => array(
'description' => __( 'Where the template originally comes from e.g. \'theme\'' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
'enum' => array(
'theme',
'plugin',
'site',
'user',
),
),
),
);
if ( 'wp_template' === $this->post_type ) {
$schema['properties']['is_custom'] = array(
'description' => __( 'Whether a template is a custom template.' ),
'type' => 'bool',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
);
}
if ( 'wp_template_part' === $this->post_type ) {
$schema['properties']['area'] = array(
'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
}
endpoints/class-wp-rest-terms-controller.php 0000644 00000102247 15153067731 0015313 0 ustar 00 <?php
/**
* REST API: WP_REST_Terms_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to managed terms associated with a taxonomy via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Terms_Controller extends WP_REST_Controller {
/**
* Taxonomy key.
*
* @since 4.7.0
* @var string
*/
protected $taxonomy;
/**
* Instance of a term meta fields object.
*
* @since 4.7.0
* @var WP_REST_Term_Meta_Fields
*/
protected $meta;
/**
* Column to have the terms be sorted by.
*
* @since 4.7.0
* @var string
*/
protected $sort_column;
/**
* Number of terms that were found.
*
* @since 4.7.0
* @var int
*/
protected $total_terms;
/**
* Whether the controller supports batching.
*
* @since 5.9.0
* @var array
*/
protected $allow_batch = array( 'v1' => true );
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $taxonomy Taxonomy key.
*/
public function __construct( $taxonomy ) {
$this->taxonomy = $taxonomy;
$tax_obj = get_taxonomy( $taxonomy );
$this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
$this->namespace = ! empty( $tax_obj->rest_namespace ) ? $tax_obj->rest_namespace : 'wp/v2';
$this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
}
/**
* Registers the routes for terms.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the term.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Required to be true, as terms do not support trashing.' ),
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if the terms for a post can be read.
*
* @since 6.0.3
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Full details about the request.
* @return bool Whether the terms for the post can be read.
*/
public function check_read_terms_permission_for_post( $post, $request ) {
// If the requested post isn't associated with this taxonomy, deny access.
if ( ! is_object_in_taxonomy( $post->post_type, $this->taxonomy ) ) {
return false;
}
// Grant access if the post is publicly viewable.
if ( is_post_publicly_viewable( $post ) ) {
return true;
}
// Otherwise grant access if the post is readable by the logged in user.
if ( current_user_can( 'read_post', $post->ID ) ) {
return true;
}
// Otherwise, deny access.
return false;
}
/**
* Checks if a request has access to read terms in the specified taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
*/
public function get_items_permissions_check( $request ) {
$tax_obj = get_taxonomy( $this->taxonomy );
if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit terms in this taxonomy.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( ! empty( $request['post'] ) ) {
$post = get_post( $request['post'] );
if ( ! $post ) {
return new WP_Error(
'rest_post_invalid_id',
__( 'Invalid post ID.' ),
array(
'status' => 400,
)
);
}
if ( ! $this->check_read_terms_permission_for_post( $post, $request ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to view terms for this post.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
}
return true;
}
/**
* Retrieves terms associated with a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'exclude' => 'exclude',
'include' => 'include',
'order' => 'order',
'orderby' => 'orderby',
'post' => 'post',
'hide_empty' => 'hide_empty',
'per_page' => 'number',
'search' => 'search',
'slug' => 'slug',
);
$prepared_args = array( 'taxonomy' => $this->taxonomy );
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $prepared_args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
if ( isset( $prepared_args['orderby'] ) && isset( $request['orderby'] ) ) {
$orderby_mappings = array(
'include_slugs' => 'slug__in',
);
if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
$prepared_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
}
}
if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
if ( 0 === $request['parent'] ) {
// Only query top-level terms.
$prepared_args['parent'] = 0;
} else {
if ( $request['parent'] ) {
$prepared_args['parent'] = $request['parent'];
}
}
}
/**
* Filters get_terms() arguments when querying terms via the REST API.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_category_query`
* - `rest_post_tag_query`
*
* Enables adding extra arguments or setting defaults for a terms
* collection request.
*
* @since 4.7.0
*
* @link https://developer.wordpress.org/reference/functions/get_terms/
*
* @param array $prepared_args Array of arguments for get_terms().
* @param WP_REST_Request $request The REST API request.
*/
$prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );
if ( ! empty( $prepared_args['post'] ) ) {
$query_result = wp_get_object_terms( $prepared_args['post'], $this->taxonomy, $prepared_args );
// Used when calling wp_count_terms() below.
$prepared_args['object_ids'] = $prepared_args['post'];
} else {
$query_result = get_terms( $prepared_args );
}
$count_args = $prepared_args;
unset( $count_args['number'], $count_args['offset'] );
$total_terms = wp_count_terms( $count_args );
// wp_count_terms() can return a falsey value when the term has no children.
if ( ! $total_terms ) {
$total_terms = 0;
}
$response = array();
foreach ( $query_result as $term ) {
$data = $this->prepare_item_for_response( $term, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $response );
// Store pagination values for headers.
$per_page = (int) $prepared_args['number'];
$page = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$response->header( 'X-WP-Total', (int) $total_terms );
$max_pages = (int) ceil( $total_terms / $per_page );
$response->header( 'X-WP-TotalPages', $max_pages );
$request_params = $request->get_query_params();
$collection_url = rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) );
$base = add_query_arg( urlencode_deep( $request_params ), $collection_url );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Get the term, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
*/
protected function get_term( $id ) {
$error = new WP_Error(
'rest_term_invalid',
__( 'Term does not exist.' ),
array( 'status' => 404 )
);
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return $error;
}
if ( (int) $id <= 0 ) {
return $error;
}
$term = get_term( (int) $id, $this->taxonomy );
if ( empty( $term ) || $term->taxonomy !== $this->taxonomy ) {
return $error;
}
return $term;
}
/**
* Checks if a request has access to read or edit the specified term.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
*/
public function get_item_permissions_check( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( 'edit' === $request['context'] && ! current_user_can( 'edit_term', $term->term_id ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to edit this term.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Gets a single term from a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a request has access to create a term.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, false or WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}
$taxonomy_obj = get_taxonomy( $this->taxonomy );
if ( ( is_taxonomy_hierarchical( $this->taxonomy )
&& ! current_user_can( $taxonomy_obj->cap->edit_terms ) )
|| ( ! is_taxonomy_hierarchical( $this->taxonomy )
&& ! current_user_can( $taxonomy_obj->cap->assign_terms ) ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create terms in this taxonomy.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates a single term in a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error(
'rest_taxonomy_not_hierarchical',
__( 'Cannot set parent term, taxonomy is not hierarchical.' ),
array( 'status' => 400 )
);
}
$parent = get_term( (int) $request['parent'], $this->taxonomy );
if ( ! $parent ) {
return new WP_Error(
'rest_term_invalid',
__( 'Parent term does not exist.' ),
array( 'status' => 400 )
);
}
}
$prepared_term = $this->prepare_item_for_database( $request );
$term = wp_insert_term( wp_slash( $prepared_term->name ), $this->taxonomy, wp_slash( (array) $prepared_term ) );
if ( is_wp_error( $term ) ) {
/*
* If we're going to inform the client that the term already exists,
* give them the identifier for future use.
*/
$term_id = $term->get_error_data( 'term_exists' );
if ( $term_id ) {
$existing_term = get_term( $term_id, $this->taxonomy );
$term->add_data( $existing_term->term_id, 'term_exists' );
$term->add_data(
array(
'status' => 400,
'term_id' => $term_id,
)
);
}
return $term;
}
$term = get_term( $term['term_id'], $this->taxonomy );
/**
* Fires after a single term is created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_insert_category`
* - `rest_insert_post_tag`
*
* @since 4.7.0
*
* @param WP_Term $term Inserted or updated term object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a term, false when updating.
*/
do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a single term is completely created or updated via the REST API.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_after_insert_category`
* - `rest_after_insert_post_tag`
*
* @since 5.0.0
*
* @param WP_Term $term Inserted or updated term object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a term, false when updating.
*/
do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );
$response = $this->prepare_item_for_response( $term, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );
return $response;
}
/**
* Checks if a request has access to update the specified term.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( ! current_user_can( 'edit_term', $term->term_id ) ) {
return new WP_Error(
'rest_cannot_update',
__( 'Sorry, you are not allowed to edit this term.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates a single term from a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
return new WP_Error(
'rest_taxonomy_not_hierarchical',
__( 'Cannot set parent term, taxonomy is not hierarchical.' ),
array( 'status' => 400 )
);
}
$parent = get_term( (int) $request['parent'], $this->taxonomy );
if ( ! $parent ) {
return new WP_Error(
'rest_term_invalid',
__( 'Parent term does not exist.' ),
array( 'status' => 400 )
);
}
}
$prepared_term = $this->prepare_item_for_database( $request );
// Only update the term if we have something to update.
if ( ! empty( $prepared_term ) ) {
$update = wp_update_term( $term->term_id, $term->taxonomy, wp_slash( (array) $prepared_term ) );
if ( is_wp_error( $update ) ) {
return $update;
}
}
$term = get_term( $term->term_id, $this->taxonomy );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$fields_update = $this->update_additional_fields_for_object( $term, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Checks if a request has access to delete the specified term.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, otherwise false or WP_Error object.
*/
public function delete_item_permissions_check( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( ! current_user_can( 'delete_term', $term->term_id ) ) {
return new WP_Error(
'rest_cannot_delete',
__( 'Sorry, you are not allowed to delete this term.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a single term from a taxonomy.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
$term = $this->get_term( $request['id'] );
if ( is_wp_error( $term ) ) {
return $term;
}
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for terms.
if ( ! $force ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "Terms do not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
$request->set_param( 'context', 'view' );
$previous = $this->prepare_item_for_response( $term, $request );
$retval = wp_delete_term( $term->term_id, $term->taxonomy );
if ( ! $retval ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The term cannot be deleted.' ),
array( 'status' => 500 )
);
}
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
/**
* Fires after a single term is deleted via the REST API.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_delete_category`
* - `rest_delete_post_tag`
*
* @since 4.7.0
*
* @param WP_Term $term The deleted term.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );
return $response;
}
/**
* Prepares a single term for create or update.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return object Term object.
*/
public function prepare_item_for_database( $request ) {
$prepared_term = new stdClass();
$schema = $this->get_item_schema();
if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
$prepared_term->name = $request['name'];
}
if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
$prepared_term->slug = $request['slug'];
}
if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
$prepared_term->taxonomy = $request['taxonomy'];
}
if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
$prepared_term->description = $request['description'];
}
if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
$parent_term_id = 0;
$requested_parent = (int) $request['parent'];
if ( $requested_parent ) {
$parent_term = get_term( $requested_parent, $this->taxonomy );
if ( $parent_term instanceof WP_Term ) {
$parent_term_id = $parent_term->term_id;
}
}
$prepared_term->parent = $parent_term_id;
}
/**
* Filters term data before inserting term via the REST API.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_pre_insert_category`
* - `rest_pre_insert_post_tag`
*
* @since 4.7.0
*
* @param object $prepared_term Term object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
}
/**
* Prepares a single term output for response.
*
* @since 4.7.0
*
* @param WP_Term $item Term object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'id', $fields, true ) ) {
$data['id'] = (int) $item->term_id;
}
if ( in_array( 'count', $fields, true ) ) {
$data['count'] = (int) $item->count;
}
if ( in_array( 'description', $fields, true ) ) {
$data['description'] = $item->description;
}
if ( in_array( 'link', $fields, true ) ) {
$data['link'] = get_term_link( $item );
}
if ( in_array( 'name', $fields, true ) ) {
$data['name'] = $item->name;
}
if ( in_array( 'slug', $fields, true ) ) {
$data['slug'] = $item->slug;
}
if ( in_array( 'taxonomy', $fields, true ) ) {
$data['taxonomy'] = $item->taxonomy;
}
if ( in_array( 'parent', $fields, true ) ) {
$data['parent'] = (int) $item->parent;
}
if ( in_array( 'meta', $fields, true ) ) {
$data['meta'] = $this->meta->get_value( $item->term_id, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $item ) );
}
/**
* Filters the term data for a REST API response.
*
* The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
*
* Possible hook names include:
*
* - `rest_prepare_category`
* - `rest_prepare_post_tag`
*
* Allows modification of the term data right before it is returned.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Term $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Prepares links for the request.
*
* @since 4.7.0
*
* @param WP_Term $term Term object.
* @return array Links for the given term.
*/
protected function prepare_links( $term ) {
$links = array(
'self' => array(
'href' => rest_url( rest_get_route_for_term( $term ) ),
),
'collection' => array(
'href' => rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) ),
),
'about' => array(
'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
),
);
if ( $term->parent ) {
$parent_term = get_term( (int) $term->parent, $term->taxonomy );
if ( $parent_term ) {
$links['up'] = array(
'href' => rest_url( rest_get_route_for_term( $parent_term ) ),
'embeddable' => true,
);
}
}
$taxonomy_obj = get_taxonomy( $term->taxonomy );
if ( empty( $taxonomy_obj->object_type ) ) {
return $links;
}
$post_type_links = array();
foreach ( $taxonomy_obj->object_type as $type ) {
$rest_path = rest_get_route_for_post_type_items( $type );
if ( empty( $rest_path ) ) {
continue;
}
$post_type_links[] = array(
'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( $rest_path ) ),
);
}
if ( ! empty( $post_type_links ) ) {
$links['https://api.w.org/post_type'] = $post_type_links;
}
return $links;
}
/**
* Retrieves the term's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the term.' ),
'type' => 'integer',
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
'count' => array(
'description' => __( 'Number of published posts for the term.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'HTML description of the term.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'link' => array(
'description' => __( 'URL of the term.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'HTML title for the term.' ),
'type' => 'string',
'context' => array( 'view', 'embed', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'required' => true,
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the term unique to its type.' ),
'type' => 'string',
'context' => array( 'view', 'embed', 'edit' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'taxonomy' => array(
'description' => __( 'Type attribution for the term.' ),
'type' => 'string',
'enum' => array( $this->taxonomy ),
'context' => array( 'view', 'embed', 'edit' ),
'readonly' => true,
),
),
);
$taxonomy = get_taxonomy( $this->taxonomy );
if ( $taxonomy->hierarchical ) {
$schema['properties']['parent'] = array(
'description' => __( 'The parent term ID.' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
);
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$taxonomy = get_taxonomy( $this->taxonomy );
$query_params['context']['default'] = 'view';
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
if ( ! $taxonomy->hierarchical ) {
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
}
$query_params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.' ),
'type' => 'string',
'default' => 'asc',
'enum' => array(
'asc',
'desc',
),
);
$query_params['orderby'] = array(
'description' => __( 'Sort collection by term attribute.' ),
'type' => 'string',
'default' => 'name',
'enum' => array(
'id',
'include',
'name',
'slug',
'include_slugs',
'term_group',
'description',
'count',
),
);
$query_params['hide_empty'] = array(
'description' => __( 'Whether to hide terms not assigned to any posts.' ),
'type' => 'boolean',
'default' => false,
);
if ( $taxonomy->hierarchical ) {
$query_params['parent'] = array(
'description' => __( 'Limit result set to terms assigned to a specific parent.' ),
'type' => 'integer',
);
}
$query_params['post'] = array(
'description' => __( 'Limit result set to terms assigned to a specific post.' ),
'type' => 'integer',
'default' => null,
);
$query_params['slug'] = array(
'description' => __( 'Limit result set to terms with one or more specific slugs.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
/**
* Filters collection parameters for the terms controller.
*
* The dynamic part of the filter `$this->taxonomy` refers to the taxonomy
* slug for the controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Term_Query parameter. Use the
* `rest_{$this->taxonomy}_query` filter to set WP_Term_Query parameters.
*
* @since 4.7.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @param WP_Taxonomy $taxonomy Taxonomy object.
*/
return apply_filters( "rest_{$this->taxonomy}_collection_params", $query_params, $taxonomy );
}
/**
* Checks that the taxonomy is valid.
*
* @since 4.7.0
*
* @param string $taxonomy Taxonomy to check.
* @return bool Whether the taxonomy is allowed for REST management.
*/
protected function check_is_taxonomy_allowed( $taxonomy ) {
$taxonomy_obj = get_taxonomy( $taxonomy );
if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
return true;
}
return false;
}
}
endpoints/class-wp-rest-themes-controller.php 0000644 00000046652 15153067731 0015455 0 ustar 00 <?php
/**
* REST API: WP_REST_Themes_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core class used to manage themes via the REST API.
*
* @since 5.0.0
*
* @see WP_REST_Controller
*/
class WP_REST_Themes_Controller extends WP_REST_Controller {
/**
* Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
* Excludes invalid directory name characters: `/:<>*?"|`.
*/
const PATTERN = '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?';
/**
* Constructor.
*
* @since 5.0.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'themes';
}
/**
* Registers the routes for themes.
*
* @since 5.0.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
sprintf( '/%s/(?P<stylesheet>%s)', $this->rest_base, self::PATTERN ),
array(
'args' => array(
'stylesheet' => array(
'description' => __( "The theme's stylesheet. This uniquely identifies the theme." ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_stylesheet_callback' ),
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Sanitize the stylesheet to decode endpoint.
*
* @since 5.9.0
*
* @param string $stylesheet The stylesheet name.
* @return string Sanitized stylesheet.
*/
public function _sanitize_stylesheet_callback( $stylesheet ) {
return urldecode( $stylesheet );
}
/**
* Checks if a given request has access to read the theme.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
*/
public function get_items_permissions_check( $request ) {
if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
return true;
}
$registered = $this->get_collection_params();
if ( isset( $registered['status'], $request['status'] ) && is_array( $request['status'] ) && array( 'active' ) === $request['status'] ) {
return $this->check_read_active_theme_permission();
}
return new WP_Error(
'rest_cannot_view_themes',
__( 'Sorry, you are not allowed to view themes.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Checks if a given request has access to read the theme.
*
* @since 5.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
*/
public function get_item_permissions_check( $request ) {
if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
return true;
}
$wp_theme = wp_get_theme( $request['stylesheet'] );
$current_theme = wp_get_theme();
if ( $this->is_same_theme( $wp_theme, $current_theme ) ) {
return $this->check_read_active_theme_permission();
}
return new WP_Error(
'rest_cannot_view_themes',
__( 'Sorry, you are not allowed to view themes.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Checks if a theme can be read.
*
* @since 5.7.0
*
* @return true|WP_Error True if the theme can be read, WP_Error object otherwise.
*/
protected function check_read_active_theme_permission() {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view_active_theme',
__( 'Sorry, you are not allowed to view the active theme.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Retrieves a single theme.
*
* @since 5.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$wp_theme = wp_get_theme( $request['stylesheet'] );
if ( ! $wp_theme->exists() ) {
return new WP_Error(
'rest_theme_not_found',
__( 'Theme not found.' ),
array( 'status' => 404 )
);
}
$data = $this->prepare_item_for_response( $wp_theme, $request );
return rest_ensure_response( $data );
}
/**
* Retrieves a collection of themes.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$themes = array();
$active_themes = wp_get_themes();
$current_theme = wp_get_theme();
$status = $request['status'];
foreach ( $active_themes as $theme_name => $theme ) {
$theme_status = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
if ( is_array( $status ) && ! in_array( $theme_status, $status, true ) ) {
continue;
}
$prepared = $this->prepare_item_for_response( $theme, $request );
$themes[] = $this->prepare_response_for_collection( $prepared );
}
$response = rest_ensure_response( $themes );
$response->header( 'X-WP-Total', count( $themes ) );
$response->header( 'X-WP-TotalPages', 1 );
return $response;
}
/**
* Prepares a single theme output for response.
*
* @since 5.0.0
* @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_Theme $item Theme object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$theme = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( rest_is_field_included( 'stylesheet', $fields ) ) {
$data['stylesheet'] = $theme->get_stylesheet();
}
if ( rest_is_field_included( 'template', $fields ) ) {
/**
* Use the get_template() method, not the 'Template' header, for finding the template.
* The 'Template' header is only good for what was written in the style.css, while
* get_template() takes into account where WordPress actually located the theme and
* whether it is actually valid.
*/
$data['template'] = $theme->get_template();
}
$plain_field_mappings = array(
'requires_php' => 'RequiresPHP',
'requires_wp' => 'RequiresWP',
'textdomain' => 'TextDomain',
'version' => 'Version',
);
foreach ( $plain_field_mappings as $field => $header ) {
if ( rest_is_field_included( $field, $fields ) ) {
$data[ $field ] = $theme->get( $header );
}
}
if ( rest_is_field_included( 'screenshot', $fields ) ) {
// Using $theme->get_screenshot() with no args to get absolute URL.
$data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : '';
}
$rich_field_mappings = array(
'author' => 'Author',
'author_uri' => 'AuthorURI',
'description' => 'Description',
'name' => 'Name',
'tags' => 'Tags',
'theme_uri' => 'ThemeURI',
);
foreach ( $rich_field_mappings as $field => $header ) {
if ( rest_is_field_included( "{$field}.raw", $fields ) ) {
$data[ $field ]['raw'] = $theme->display( $header, false, true );
}
if ( rest_is_field_included( "{$field}.rendered", $fields ) ) {
$data[ $field ]['rendered'] = $theme->display( $header );
}
}
$current_theme = wp_get_theme();
if ( rest_is_field_included( 'status', $fields ) ) {
$data['status'] = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
}
if ( rest_is_field_included( 'theme_supports', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
foreach ( get_registered_theme_features() as $feature => $config ) {
if ( ! is_array( $config['show_in_rest'] ) ) {
continue;
}
$name = $config['show_in_rest']['name'];
if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) {
continue;
}
if ( ! current_theme_supports( $feature ) ) {
$data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default'];
continue;
}
$support = get_theme_support( $feature );
if ( isset( $config['show_in_rest']['prepare_callback'] ) ) {
$prepare = $config['show_in_rest']['prepare_callback'];
} else {
$prepare = array( $this, 'prepare_theme_support' );
}
$prepared = $prepare( $support, $config, $feature, $request );
if ( is_wp_error( $prepared ) ) {
continue;
}
$data['theme_supports'][ $name ] = $prepared;
}
}
if ( rest_is_field_included( 'is_block_theme', $fields ) ) {
$data['is_block_theme'] = $theme->is_block_theme();
}
$data = $this->add_additional_fields_to_object( $data, $request );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $theme ) );
}
/**
* Filters theme data returned from the REST API.
*
* @since 5.0.0
*
* @param WP_REST_Response $response The response object.
* @param WP_Theme $theme Theme object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_theme', $response, $theme, $request );
}
/**
* Prepares links for the request.
*
* @since 5.7.0
*
* @param WP_Theme $theme Theme data.
* @return array Links for the given block type.
*/
protected function prepare_links( $theme ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $theme->get_stylesheet() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( $this->is_same_theme( $theme, wp_get_theme() ) ) {
// This creates a record for the active theme if not existent.
$id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
} else {
$user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme );
$id = isset( $user_cpt['ID'] ) ? $user_cpt['ID'] : null;
}
if ( $id ) {
$links['https://api.w.org/user-global-styles'] = array(
'href' => rest_url( 'wp/v2/global-styles/' . $id ),
);
}
return $links;
}
/**
* Helper function to compare two themes.
*
* @since 5.7.0
*
* @param WP_Theme $theme_a First theme to compare.
* @param WP_Theme $theme_b Second theme to compare.
* @return bool
*/
protected function is_same_theme( $theme_a, $theme_b ) {
return $theme_a->get_stylesheet() === $theme_b->get_stylesheet();
}
/**
* Prepares the theme support value for inclusion in the REST API response.
*
* @since 5.5.0
*
* @param mixed $support The raw value from get_theme_support().
* @param array $args The feature's registration args.
* @param string $feature The feature name.
* @param WP_REST_Request $request The request object.
* @return mixed The prepared support value.
*/
protected function prepare_theme_support( $support, $args, $feature, $request ) {
$schema = $args['show_in_rest']['schema'];
if ( 'boolean' === $schema['type'] ) {
return true;
}
if ( is_array( $support ) && ! $args['variadic'] ) {
$support = $support[0];
}
return rest_sanitize_value_from_schema( $support, $schema );
}
/**
* Retrieves the theme's schema, conforming to JSON Schema.
*
* @since 5.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'theme',
'type' => 'object',
'properties' => array(
'stylesheet' => array(
'description' => __( 'The theme\'s stylesheet. This uniquely identifies the theme.' ),
'type' => 'string',
'readonly' => true,
),
'template' => array(
'description' => __( 'The theme\'s template. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet.' ),
'type' => 'string',
'readonly' => true,
),
'author' => array(
'description' => __( 'The theme author.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The theme author\'s name, as found in the theme header.' ),
'type' => 'string',
),
'rendered' => array(
'description' => __( 'HTML for the theme author, transformed for display.' ),
'type' => 'string',
),
),
),
'author_uri' => array(
'description' => __( 'The website of the theme author.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The website of the theme author, as found in the theme header.' ),
'type' => 'string',
'format' => 'uri',
),
'rendered' => array(
'description' => __( 'The website of the theme author, transformed for display.' ),
'type' => 'string',
'format' => 'uri',
),
),
),
'description' => array(
'description' => __( 'A description of the theme.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The theme description, as found in the theme header.' ),
'type' => 'string',
),
'rendered' => array(
'description' => __( 'The theme description, transformed for display.' ),
'type' => 'string',
),
),
),
'is_block_theme' => array(
'description' => __( 'Whether the theme is a block-based theme.' ),
'type' => 'boolean',
'readonly' => true,
),
'name' => array(
'description' => __( 'The name of the theme.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The theme name, as found in the theme header.' ),
'type' => 'string',
),
'rendered' => array(
'description' => __( 'The theme name, transformed for display.' ),
'type' => 'string',
),
),
),
'requires_php' => array(
'description' => __( 'The minimum PHP version required for the theme to work.' ),
'type' => 'string',
'readonly' => true,
),
'requires_wp' => array(
'description' => __( 'The minimum WordPress version required for the theme to work.' ),
'type' => 'string',
'readonly' => true,
),
'screenshot' => array(
'description' => __( 'The theme\'s screenshot URL.' ),
'type' => 'string',
'format' => 'uri',
'readonly' => true,
),
'tags' => array(
'description' => __( 'Tags indicating styles and features of the theme.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The theme tags, as found in the theme header.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
),
'rendered' => array(
'description' => __( 'The theme tags, transformed for display.' ),
'type' => 'string',
),
),
),
'textdomain' => array(
'description' => __( 'The theme\'s text domain.' ),
'type' => 'string',
'readonly' => true,
),
'theme_supports' => array(
'description' => __( 'Features supported by this theme.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(),
),
'theme_uri' => array(
'description' => __( 'The URI of the theme\'s webpage.' ),
'type' => 'object',
'readonly' => true,
'properties' => array(
'raw' => array(
'description' => __( 'The URI of the theme\'s webpage, as found in the theme header.' ),
'type' => 'string',
'format' => 'uri',
),
'rendered' => array(
'description' => __( 'The URI of the theme\'s webpage, transformed for display.' ),
'type' => 'string',
'format' => 'uri',
),
),
),
'version' => array(
'description' => __( 'The theme\'s current version.' ),
'type' => 'string',
'readonly' => true,
),
'status' => array(
'description' => __( 'A named status for the theme.' ),
'type' => 'string',
'enum' => array( 'inactive', 'active' ),
),
),
);
foreach ( get_registered_theme_features() as $feature => $config ) {
if ( ! is_array( $config['show_in_rest'] ) ) {
continue;
}
$name = $config['show_in_rest']['name'];
$schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema'];
}
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the search params for the themes collection.
*
* @since 5.0.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = array(
'status' => array(
'description' => __( 'Limit result set to themes assigned one or more statuses.' ),
'type' => 'array',
'items' => array(
'enum' => array( 'active', 'inactive' ),
'type' => 'string',
),
),
);
/**
* Filters REST API collection parameters for the themes controller.
*
* @since 5.0.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_themes_collection_params', $query_params );
}
/**
* Sanitizes and validates the list of theme status.
*
* @since 5.0.0
* @deprecated 5.7.0
*
* @param string|array $statuses One or more theme statuses.
* @param WP_REST_Request $request Full details about the request.
* @param string $parameter Additional parameter to pass to validation.
* @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
*/
public function sanitize_theme_status( $statuses, $request, $parameter ) {
_deprecated_function( __METHOD__, '5.7.0' );
$statuses = wp_parse_slug_list( $statuses );
foreach ( $statuses as $status ) {
$result = rest_validate_request_arg( $status, $request, $parameter );
if ( is_wp_error( $result ) ) {
return $result;
}
}
return $statuses;
}
}
endpoints/class-wp-rest-url-details-controller.php 0000644 00000050114 15153067731 0016401 0 ustar 00 <?php
/**
* REST API: WP_REST_URL_Details_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.9.0
*/
/**
* Controller which provides REST endpoint for retrieving information
* from a remote site's HTML response.
*
* @since 5.9.0
*
* @see WP_REST_Controller
*/
class WP_REST_URL_Details_Controller extends WP_REST_Controller {
/**
* Constructs the controller.
*
* @since 5.9.0
*/
public function __construct() {
$this->namespace = 'wp-block-editor/v1';
$this->rest_base = 'url-details';
}
/**
* Registers the necessary REST API routes.
*
* @since 5.9.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'parse_url_details' ),
'args' => array(
'url' => array(
'required' => true,
'description' => __( 'The URL to process.' ),
'validate_callback' => 'wp_http_validate_url',
'sanitize_callback' => 'sanitize_url',
'type' => 'string',
'format' => 'uri',
),
),
'permission_callback' => array( $this, 'permissions_check' ),
'schema' => array( $this, 'get_public_item_schema' ),
),
)
);
}
/**
* Retrieves the item's schema, conforming to JSON Schema.
*
* @since 5.9.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'url-details',
'type' => 'object',
'properties' => array(
'title' => array(
'description' => sprintf(
/* translators: %s: HTML title tag. */
__( 'The contents of the %s element from the URL.' ),
'<title>'
),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'icon' => array(
'description' => sprintf(
/* translators: %s: HTML link tag. */
__( 'The favicon image link of the %s element from the URL.' ),
'<link rel="icon">'
),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'description' => array(
'description' => sprintf(
/* translators: %s: HTML meta tag. */
__( 'The content of the %s element from the URL.' ),
'<meta name="description">'
),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'image' => array(
'description' => sprintf(
/* translators: 1: HTML meta tag, 2: HTML meta tag. */
__( 'The Open Graph image link of the %1$s or %2$s element from the URL.' ),
'<meta property="og:image">',
'<meta property="og:image:url">'
),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the contents of the title tag from the HTML response.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error The parsed details as a response object. WP_Error if there are errors.
*/
public function parse_url_details( $request ) {
$url = untrailingslashit( $request['url'] );
if ( empty( $url ) ) {
return new WP_Error( 'rest_invalid_url', __( 'Invalid URL' ), array( 'status' => 404 ) );
}
// Transient per URL.
$cache_key = $this->build_cache_key_for_url( $url );
// Attempt to retrieve cached response.
$cached_response = $this->get_cache( $cache_key );
if ( ! empty( $cached_response ) ) {
$remote_url_response = $cached_response;
} else {
$remote_url_response = $this->get_remote_url( $url );
// Exit if we don't have a valid body or it's empty.
if ( is_wp_error( $remote_url_response ) || empty( $remote_url_response ) ) {
return $remote_url_response;
}
// Cache the valid response.
$this->set_cache( $cache_key, $remote_url_response );
}
$html_head = $this->get_document_head( $remote_url_response );
$meta_elements = $this->get_meta_with_content_elements( $html_head );
$data = $this->add_additional_fields_to_object(
array(
'title' => $this->get_title( $html_head ),
'icon' => $this->get_icon( $html_head, $url ),
'description' => $this->get_description( $meta_elements ),
'image' => $this->get_image( $meta_elements, $url ),
),
$request
);
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filters the URL data for the response.
*
* @since 5.9.0
*
* @param WP_REST_Response $response The response object.
* @param string $url The requested URL.
* @param WP_REST_Request $request Request object.
* @param string $remote_url_response HTTP response body from the remote URL.
*/
return apply_filters( 'rest_prepare_url_details', $response, $url, $request, $remote_url_response );
}
/**
* Checks whether a given request has permission to read remote URLs.
*
* @since 5.9.0
*
* @return WP_Error|bool True if the request has permission, else WP_Error.
*/
public function permissions_check() {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_cannot_view_url_details',
__( 'Sorry, you are not allowed to process remote URLs.' ),
array( 'status' => rest_authorization_required_code() )
);
}
/**
* Retrieves the document title from a remote URL.
*
* @since 5.9.0
*
* @param string $url The website URL whose HTML to access.
* @return string|WP_Error The HTTP response from the remote URL on success.
* WP_Error if no response or no content.
*/
private function get_remote_url( $url ) {
/*
* Provide a modified UA string to workaround web properties which block WordPress "Pingbacks".
* Why? The UA string used for pingback requests contains `WordPress/` which is very similar
* to that used as the default UA string by the WP HTTP API. Therefore requests from this
* REST endpoint are being unintentionally blocked as they are misidentified as pingback requests.
* By slightly modifying the UA string, but still retaining the "WordPress" identification (via "WP")
* we are able to work around this issue.
* Example UA string: `WP-URLDetails/5.9-alpha-51389 (+http://localhost:8888)`.
*/
$modified_user_agent = 'WP-URLDetails/' . get_bloginfo( 'version' ) . ' (+' . get_bloginfo( 'url' ) . ')';
$args = array(
'limit_response_size' => 150 * KB_IN_BYTES,
'user-agent' => $modified_user_agent,
);
/**
* Filters the HTTP request args for URL data retrieval.
*
* Can be used to adjust response size limit and other WP_Http::request() args.
*
* @since 5.9.0
*
* @param array $args Arguments used for the HTTP request.
* @param string $url The attempted URL.
*/
$args = apply_filters( 'rest_url_details_http_request_args', $args, $url );
$response = wp_safe_remote_get( $url, $args );
if ( WP_Http::OK !== wp_remote_retrieve_response_code( $response ) ) {
// Not saving the error response to cache since the error might be temporary.
return new WP_Error(
'no_response',
__( 'URL not found. Response returned a non-200 status code for this URL.' ),
array( 'status' => WP_Http::NOT_FOUND )
);
}
$remote_body = wp_remote_retrieve_body( $response );
if ( empty( $remote_body ) ) {
return new WP_Error(
'no_content',
__( 'Unable to retrieve body from response at this URL.' ),
array( 'status' => WP_Http::NOT_FOUND )
);
}
return $remote_body;
}
/**
* Parses the title tag contents from the provided HTML.
*
* @since 5.9.0
*
* @param string $html The HTML from the remote website at URL.
* @return string The title tag contents on success. Empty string if not found.
*/
private function get_title( $html ) {
$pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is';
preg_match( $pattern, $html, $match_title );
if ( empty( $match_title[1] ) || ! is_string( $match_title[1] ) ) {
return '';
}
$title = trim( $match_title[1] );
return $this->prepare_metadata_for_output( $title );
}
/**
* Parses the site icon from the provided HTML.
*
* @since 5.9.0
*
* @param string $html The HTML from the remote website at URL.
* @param string $url The target website URL.
* @return string The icon URI on success. Empty string if not found.
*/
private function get_icon( $html, $url ) {
// Grab the icon's link element.
$pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU';
preg_match( $pattern, $html, $element );
if ( empty( $element[0] ) || ! is_string( $element[0] ) ) {
return '';
}
$element = trim( $element[0] );
// Get the icon's href value.
$pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU';
preg_match( $pattern, $element, $icon );
if ( empty( $icon[2] ) || ! is_string( $icon[2] ) ) {
return '';
}
$icon = trim( $icon[2] );
// If the icon is a data URL, return it.
$parsed_icon = parse_url( $icon );
if ( isset( $parsed_icon['scheme'] ) && 'data' === $parsed_icon['scheme'] ) {
return $icon;
}
// Attempt to convert relative URLs to absolute.
if ( ! is_string( $url ) || '' === $url ) {
return $icon;
}
$parsed_url = parse_url( $url );
if ( isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] ) ) {
$root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
$icon = WP_Http::make_absolute_url( $icon, $root_url );
}
return $icon;
}
/**
* Parses the meta description from the provided HTML.
*
* @since 5.9.0
*
* @param array $meta_elements {
* A multi-dimensional indexed array on success, else empty array.
*
* @type string[] $0 Meta elements with a content attribute.
* @type string[] $1 Content attribute's opening quotation mark.
* @type string[] $2 Content attribute's value for each meta element.
* }
* @return string The meta description contents on success. Empty string if not found.
*/
private function get_description( $meta_elements ) {
// Bail out if there are no meta elements.
if ( empty( $meta_elements[0] ) ) {
return '';
}
$description = $this->get_metadata_from_meta_element(
$meta_elements,
'name',
'(?:description|og:description)'
);
// Bail out if description not found.
if ( '' === $description ) {
return '';
}
return $this->prepare_metadata_for_output( $description );
}
/**
* Parses the Open Graph (OG) Image from the provided HTML.
*
* See: https://ogp.me/.
*
* @since 5.9.0
*
* @param array $meta_elements {
* A multi-dimensional indexed array on success, else empty array.
*
* @type string[] $0 Meta elements with a content attribute.
* @type string[] $1 Content attribute's opening quotation mark.
* @type string[] $2 Content attribute's value for each meta element.
* }
* @param string $url The target website URL.
* @return string The OG image on success. Empty string if not found.
*/
private function get_image( $meta_elements, $url ) {
$image = $this->get_metadata_from_meta_element(
$meta_elements,
'property',
'(?:og:image|og:image:url)'
);
// Bail out if image not found.
if ( '' === $image ) {
return '';
}
// Attempt to convert relative URLs to absolute.
$parsed_url = parse_url( $url );
if ( isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] ) ) {
$root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
$image = WP_Http::make_absolute_url( $image, $root_url );
}
return $image;
}
/**
* Prepares the metadata by:
* - stripping all HTML tags and tag entities.
* - converting non-tag entities into characters.
*
* @since 5.9.0
*
* @param string $metadata The metadata content to prepare.
* @return string The prepared metadata.
*/
private function prepare_metadata_for_output( $metadata ) {
$metadata = html_entity_decode( $metadata, ENT_QUOTES, get_bloginfo( 'charset' ) );
$metadata = wp_strip_all_tags( $metadata );
return $metadata;
}
/**
* Utility function to build cache key for a given URL.
*
* @since 5.9.0
*
* @param string $url The URL for which to build a cache key.
* @return string The cache key.
*/
private function build_cache_key_for_url( $url ) {
return 'g_url_details_response_' . md5( $url );
}
/**
* Utility function to retrieve a value from the cache at a given key.
*
* @since 5.9.0
*
* @param string $key The cache key.
* @return mixed The value from the cache.
*/
private function get_cache( $key ) {
return get_site_transient( $key );
}
/**
* Utility function to cache a given data set at a given cache key.
*
* @since 5.9.0
*
* @param string $key The cache key under which to store the value.
* @param string $data The data to be stored at the given cache key.
* @return bool True when transient set. False if not set.
*/
private function set_cache( $key, $data = '' ) {
$ttl = HOUR_IN_SECONDS;
/**
* Filters the cache expiration.
*
* Can be used to adjust the time until expiration in seconds for the cache
* of the data retrieved for the given URL.
*
* @since 5.9.0
*
* @param int $ttl The time until cache expiration in seconds.
*/
$cache_expiration = apply_filters( 'rest_url_details_cache_expiration', $ttl );
return set_site_transient( $key, $data, $cache_expiration );
}
/**
* Retrieves the head element section.
*
* @since 5.9.0
*
* @param string $html The string of HTML to parse.
* @return string The `<head>..</head>` section on success. Given `$html` if not found.
*/
private function get_document_head( $html ) {
$head_html = $html;
// Find the opening `<head>` tag.
$head_start = strpos( $html, '<head' );
if ( false === $head_start ) {
// Didn't find it. Return the original HTML.
return $html;
}
// Find the closing `</head>` tag.
$head_end = strpos( $head_html, '</head>' );
if ( false === $head_end ) {
// Didn't find it. Find the opening `<body>` tag.
$head_end = strpos( $head_html, '<body' );
// Didn't find it. Return the original HTML.
if ( false === $head_end ) {
return $html;
}
}
// Extract the HTML from opening tag to the closing tag. Then add the closing tag.
$head_html = substr( $head_html, $head_start, $head_end );
$head_html .= '</head>';
return $head_html;
}
/**
* Gets all the meta tag elements that have a 'content' attribute.
*
* @since 5.9.0
*
* @param string $html The string of HTML to be parsed.
* @return array {
* A multi-dimensional indexed array on success, else empty array.
*
* @type string[] $0 Meta elements with a content attribute.
* @type string[] $1 Content attribute's opening quotation mark.
* @type string[] $2 Content attribute's value for each meta element.
* }
*/
private function get_meta_with_content_elements( $html ) {
/*
* Parse all meta elements with a content attribute.
*
* Why first search for the content attribute rather than directly searching for name=description element?
* tl;dr The content attribute's value will be truncated when it contains a > symbol.
*
* The content attribute's value (i.e. the description to get) can have HTML in it and be well-formed as
* it's a string to the browser. Imagine what happens when attempting to match for the name=description
* first. Hmm, if a > or /> symbol is in the content attribute's value, then it terminates the match
* as the element's closing symbol. But wait, it's in the content attribute and is not the end of the
* element. This is a limitation of using regex. It can't determine "wait a minute this is inside of quotation".
* If this happens, what gets matched is not the entire element or all of the content.
*
* Why not search for the name=description and then content="(.*)"?
* The attribute order could be opposite. Plus, additional attributes may exist including being between
* the name and content attributes.
*
* Why not lookahead?
* Lookahead is not constrained to stay within the element. The first <meta it finds may not include
* the name or content, but rather could be from a different element downstream.
*/
$pattern = '#<meta\s' .
/*
* Allows for additional attributes before the content attribute.
* Searches for anything other than > symbol.
*/
'[^>]*' .
/*
* Find the content attribute. When found, capture its value (.*).
*
* Allows for (a) single or double quotes and (b) whitespace in the value.
*
* Why capture the opening quotation mark, i.e. (["\']), and then backreference,
* i.e \1, for the closing quotation mark?
* To ensure the closing quotation mark matches the opening one. Why? Attribute values
* can contain quotation marks, such as an apostrophe in the content.
*/
'content=(["\']??)(.*)\1' .
/*
* Allows for additional attributes after the content attribute.
* Searches for anything other than > symbol.
*/
'[^>]*' .
/*
* \/?> searches for the closing > symbol, which can be in either /> or > format.
* # ends the pattern.
*/
'\/?>#' .
/*
* These are the options:
* - i : case insensitive
* - s : allows newline characters for the . match (needed for multiline elements)
* - U means non-greedy matching
*/
'isU';
preg_match_all( $pattern, $html, $elements );
return $elements;
}
/**
* Gets the metadata from a target meta element.
*
* @since 5.9.0
*
* @param array $meta_elements {
* A multi-dimensional indexed array on success, else empty array.
*
* @type string[] $0 Meta elements with a content attribute.
* @type string[] $1 Content attribute's opening quotation mark.
* @type string[] $2 Content attribute's value for each meta element.
* }
* @param string $attr Attribute that identifies the element with the target metadata.
* @param string $attr_value The attribute's value that identifies the element with the target metadata.
* @return string The metadata on success. Empty string if not found.
*/
private function get_metadata_from_meta_element( $meta_elements, $attr, $attr_value ) {
// Bail out if there are no meta elements.
if ( empty( $meta_elements[0] ) ) {
return '';
}
$metadata = '';
$pattern = '#' .
/*
* Target this attribute and value to find the metadata element.
*
* Allows for (a) no, single, double quotes and (b) whitespace in the value.
*
* Why capture the opening quotation mark, i.e. (["\']), and then backreference,
* i.e \1, for the closing quotation mark?
* To ensure the closing quotation mark matches the opening one. Why? Attribute values
* can contain quotation marks, such as an apostrophe in the content.
*/
$attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' .
/*
* These are the options:
* - i : case insensitive
* - s : allows newline characters for the . match (needed for multiline elements)
* - U means non-greedy matching
*/
'#isU';
// Find the metadata element.
foreach ( $meta_elements[0] as $index => $element ) {
preg_match( $pattern, $element, $match );
// This is not the metadata element. Skip it.
if ( empty( $match ) ) {
continue;
}
/*
* Found the metadata element.
* Get the metadata from its matching content array.
*/
if ( isset( $meta_elements[2][ $index ] ) && is_string( $meta_elements[2][ $index ] ) ) {
$metadata = trim( $meta_elements[2][ $index ] );
}
break;
}
return $metadata;
}
}
endpoints/class-wp-rest-users-controller.php 0000644 00000134615 15153067731 0015326 0 ustar 00 <?php
/**
* REST API: WP_REST_Users_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage users via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Controller
*/
class WP_REST_Users_Controller extends WP_REST_Controller {
/**
* Instance of a user meta fields object.
*
* @since 4.7.0
* @var WP_REST_User_Meta_Fields
*/
protected $meta;
/**
* Constructor.
*
* @since 4.7.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'users';
$this->meta = new WP_REST_User_Meta_Fields();
}
/**
* Registers the routes for users.
*
* @since 4.7.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the user.' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Required to be true, as users do not support trashing.' ),
),
'reassign' => array(
'type' => 'integer',
'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.' ),
'required' => true,
'sanitize_callback' => array( $this, 'check_reassign' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/me',
array(
array(
'methods' => WP_REST_Server::READABLE,
'permission_callback' => '__return_true',
'callback' => array( $this, 'get_current_item' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_current_item' ),
'permission_callback' => array( $this, 'update_current_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_current_item' ),
'permission_callback' => array( $this, 'delete_current_item_permissions_check' ),
'args' => array(
'force' => array(
'type' => 'boolean',
'default' => false,
'description' => __( 'Required to be true, as users do not support trashing.' ),
),
'reassign' => array(
'type' => 'integer',
'description' => __( 'Reassign the deleted user\'s posts and links to this user ID.' ),
'required' => true,
'sanitize_callback' => array( $this, 'check_reassign' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks for a valid value for the reassign parameter when deleting users.
*
* The value can be an integer, 'false', false, or ''.
*
* @since 4.7.0
*
* @param int|bool $value The value passed to the reassign parameter.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter that is being sanitized.
* @return int|bool|WP_Error
*/
public function check_reassign( $value, $request, $param ) {
if ( is_numeric( $value ) ) {
return $value;
}
if ( empty( $value ) || false === $value || 'false' === $value ) {
return false;
}
return new WP_Error(
'rest_invalid_param',
__( 'Invalid user parameter(s).' ),
array( 'status' => 400 )
);
}
/**
* Permissions check for getting all users.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, otherwise WP_Error object.
*/
public function get_items_permissions_check( $request ) {
// Check if roles is specified in GET request and if user can list users.
if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_user_cannot_view',
__( 'Sorry, you are not allowed to filter users by role.' ),
array( 'status' => rest_authorization_required_code() )
);
}
// Check if capabilities is specified in GET request and if user can list users.
if ( ! empty( $request['capabilities'] ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_user_cannot_view',
__( 'Sorry, you are not allowed to filter users by capability.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_forbidden_context',
__( 'Sorry, you are not allowed to list users.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_forbidden_orderby',
__( 'Sorry, you are not allowed to order users by this parameter.' ),
array( 'status' => rest_authorization_required_code() )
);
}
if ( 'authors' === $request['who'] ) {
$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
foreach ( $types as $type ) {
if ( post_type_supports( $type->name, 'author' )
&& current_user_can( $type->cap->edit_posts ) ) {
return true;
}
}
return new WP_Error(
'rest_forbidden_who',
__( 'Sorry, you are not allowed to query users by this parameter.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves all users.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'exclude' => 'exclude',
'include' => 'include',
'order' => 'order',
'per_page' => 'number',
'search' => 'search',
'roles' => 'role__in',
'capabilities' => 'capability__in',
'slug' => 'nicename__in',
);
$prepared_args = array();
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $prepared_args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
if ( isset( $registered['orderby'] ) ) {
$orderby_possibles = array(
'id' => 'ID',
'include' => 'include',
'name' => 'display_name',
'registered_date' => 'registered',
'slug' => 'user_nicename',
'include_slugs' => 'nicename__in',
'email' => 'user_email',
'url' => 'user_url',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
}
if ( isset( $registered['who'] ) && ! empty( $request['who'] ) && 'authors' === $request['who'] ) {
$prepared_args['who'] = 'authors';
} elseif ( ! current_user_can( 'list_users' ) ) {
$prepared_args['has_published_posts'] = get_post_types( array( 'show_in_rest' => true ), 'names' );
}
if ( ! empty( $request['has_published_posts'] ) ) {
$prepared_args['has_published_posts'] = ( true === $request['has_published_posts'] )
? get_post_types( array( 'show_in_rest' => true ), 'names' )
: (array) $request['has_published_posts'];
}
if ( ! empty( $prepared_args['search'] ) ) {
if ( ! current_user_can( 'list_users' ) ) {
$prepared_args['search_columns'] = array( 'ID', 'user_login', 'user_nicename', 'display_name' );
}
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
}
/**
* Filters WP_User_Query arguments when querying users via the REST API.
*
* @link https://developer.wordpress.org/reference/classes/wp_user_query/
*
* @since 4.7.0
*
* @param array $prepared_args Array of arguments for WP_User_Query.
* @param WP_REST_Request $request The REST API request.
*/
$prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );
$query = new WP_User_Query( $prepared_args );
$users = array();
foreach ( $query->results as $user ) {
$data = $this->prepare_item_for_response( $user, $request );
$users[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $users );
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
$page = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$prepared_args['fields'] = 'ID';
$total_users = $query->get_total();
if ( $total_users < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $prepared_args['number'], $prepared_args['offset'] );
$count_query = new WP_User_Query( $prepared_args );
$total_users = $count_query->get_total();
}
$response->header( 'X-WP-Total', (int) $total_users );
$max_pages = (int) ceil( $total_users / $per_page );
$response->header( 'X-WP-TotalPages', $max_pages );
$base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Get the user, if the ID is valid.
*
* @since 4.7.2
*
* @param int $id Supplied ID.
* @return WP_User|WP_Error True if ID is valid, WP_Error otherwise.
*/
protected function get_user( $id ) {
$error = new WP_Error(
'rest_user_invalid_id',
__( 'Invalid user ID.' ),
array( 'status' => 404 )
);
if ( (int) $id <= 0 ) {
return $error;
}
$user = get_userdata( (int) $id );
if ( empty( $user ) || ! $user->exists() ) {
return $error;
}
if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) {
return $error;
}
return $user;
}
/**
* Checks if a given request has access to read a user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
*/
public function get_item_permissions_check( $request ) {
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
$types = get_post_types( array( 'show_in_rest' => true ), 'names' );
if ( get_current_user_id() === $user->ID ) {
return true;
}
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_user_cannot_view',
__( 'Sorry, you are not allowed to list users.' ),
array( 'status' => rest_authorization_required_code() )
);
} elseif ( ! count_user_posts( $user->ID, $types ) && ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) ) {
return new WP_Error(
'rest_user_cannot_view',
__( 'Sorry, you are not allowed to list users.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Retrieves a single user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
$user = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $user );
return $response;
}
/**
* Retrieves the current user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_current_item( $request ) {
$current_user_id = get_current_user_id();
if ( empty( $current_user_id ) ) {
return new WP_Error(
'rest_not_logged_in',
__( 'You are not currently logged in.' ),
array( 'status' => 401 )
);
}
$user = wp_get_current_user();
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Checks if a given request has access create users.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
if ( ! current_user_can( 'create_users' ) ) {
return new WP_Error(
'rest_cannot_create_user',
__( 'Sorry, you are not allowed to create new users.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Creates a single user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error(
'rest_user_exists',
__( 'Cannot create existing user.' ),
array( 'status' => 400 )
);
}
$schema = $this->get_item_schema();
if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
$check_permission = $this->check_role_update( $request['id'], $request['roles'] );
if ( is_wp_error( $check_permission ) ) {
return $check_permission;
}
}
$user = $this->prepare_item_for_database( $request );
if ( is_multisite() ) {
$ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );
if ( is_wp_error( $ret['errors'] ) && $ret['errors']->has_errors() ) {
$error = new WP_Error(
'rest_invalid_param',
__( 'Invalid user parameter(s).' ),
array( 'status' => 400 )
);
foreach ( $ret['errors']->errors as $code => $messages ) {
foreach ( $messages as $message ) {
$error->add( $code, $message );
}
$error_data = $error->get_error_data( $code );
if ( $error_data ) {
$error->add_data( $error_data, $code );
}
}
return $error;
}
}
if ( is_multisite() ) {
$user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );
if ( ! $user_id ) {
return new WP_Error(
'rest_user_create',
__( 'Error creating new user.' ),
array( 'status' => 500 )
);
}
$user->ID = $user_id;
$user_id = wp_update_user( wp_slash( (array) $user ) );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
$result = add_user_to_blog( get_site()->id, $user_id, '' );
if ( is_wp_error( $result ) ) {
return $result;
}
} else {
$user_id = wp_insert_user( wp_slash( (array) $user ) );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
}
$user = get_user_by( 'id', $user_id );
/**
* Fires immediately after a user is created or updated via the REST API.
*
* @since 4.7.0
*
* @param WP_User $user Inserted or updated user object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a user, false when updating.
*/
do_action( 'rest_insert_user', $user, $request, true );
if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
array_map( array( $user, 'add_role' ), $request['roles'] );
}
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $user_id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$user = get_user_by( 'id', $user_id );
$fields_update = $this->update_additional_fields_for_object( $user, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/**
* Fires after a user is completely created or updated via the REST API.
*
* @since 5.0.0
*
* @param WP_User $user Inserted or updated user object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a user, false when updating.
*/
do_action( 'rest_after_insert_user', $user, $request, true );
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );
return $response;
}
/**
* Checks if a given request has access to update a user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! empty( $request['roles'] ) ) {
if ( ! current_user_can( 'promote_user', $user->ID ) ) {
return new WP_Error(
'rest_cannot_edit_roles',
__( 'Sorry, you are not allowed to edit roles of this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
$request_params = array_keys( $request->get_params() );
sort( $request_params );
/*
* If only 'id' and 'roles' are specified (we are only trying to
* edit roles), then only the 'promote_user' cap is required.
*/
if ( array( 'id', 'roles' ) === $request_params ) {
return true;
}
}
if ( ! current_user_can( 'edit_user', $user->ID ) ) {
return new WP_Error(
'rest_cannot_edit',
__( 'Sorry, you are not allowed to edit this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Updates a single user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
$id = $user->ID;
$owner_id = false;
if ( is_string( $request['email'] ) ) {
$owner_id = email_exists( $request['email'] );
}
if ( $owner_id && $owner_id !== $id ) {
return new WP_Error(
'rest_user_invalid_email',
__( 'Invalid email address.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
return new WP_Error(
'rest_user_invalid_argument',
__( 'Username is not editable.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
return new WP_Error(
'rest_user_invalid_slug',
__( 'Invalid slug.' ),
array( 'status' => 400 )
);
}
if ( ! empty( $request['roles'] ) ) {
$check_permission = $this->check_role_update( $id, $request['roles'] );
if ( is_wp_error( $check_permission ) ) {
return $check_permission;
}
}
$user = $this->prepare_item_for_database( $request );
// Ensure we're operating on the same user we already checked.
$user->ID = $id;
$user_id = wp_update_user( wp_slash( (array) $user ) );
if ( is_wp_error( $user_id ) ) {
return $user_id;
}
$user = get_user_by( 'id', $user_id );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */
do_action( 'rest_insert_user', $user, $request, false );
if ( ! empty( $request['roles'] ) ) {
array_map( array( $user, 'add_role' ), $request['roles'] );
}
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
$meta_update = $this->meta->update_value( $request['meta'], $id );
if ( is_wp_error( $meta_update ) ) {
return $meta_update;
}
}
$user = get_user_by( 'id', $user_id );
$fields_update = $this->update_additional_fields_for_object( $user, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */
do_action( 'rest_after_insert_user', $user, $request, false );
$response = $this->prepare_item_for_response( $user, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Checks if a given request has access to update the current user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
*/
public function update_current_item_permissions_check( $request ) {
$request['id'] = get_current_user_id();
return $this->update_item_permissions_check( $request );
}
/**
* Updates the current user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_current_item( $request ) {
$request['id'] = get_current_user_id();
return $this->update_item( $request );
}
/**
* Checks if a given request has access delete a user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
if ( ! current_user_can( 'delete_user', $user->ID ) ) {
return new WP_Error(
'rest_user_cannot_delete',
__( 'Sorry, you are not allowed to delete this user.' ),
array( 'status' => rest_authorization_required_code() )
);
}
return true;
}
/**
* Deletes a single user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
// We don't support delete requests in multisite.
if ( is_multisite() ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The user cannot be deleted.' ),
array( 'status' => 501 )
);
}
$user = $this->get_user( $request['id'] );
if ( is_wp_error( $user ) ) {
return $user;
}
$id = $user->ID;
$reassign = false === $request['reassign'] ? null : absint( $request['reassign'] );
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for users.
if ( ! $force ) {
return new WP_Error(
'rest_trash_not_supported',
/* translators: %s: force=true */
sprintf( __( "Users do not support trashing. Set '%s' to delete." ), 'force=true' ),
array( 'status' => 501 )
);
}
if ( ! empty( $reassign ) ) {
if ( $reassign === $id || ! get_userdata( $reassign ) ) {
return new WP_Error(
'rest_user_invalid_reassign',
__( 'Invalid user ID for reassignment.' ),
array( 'status' => 400 )
);
}
}
$request->set_param( 'context', 'edit' );
$previous = $this->prepare_item_for_response( $user, $request );
// Include user admin functions to get access to wp_delete_user().
require_once ABSPATH . 'wp-admin/includes/user.php';
$result = wp_delete_user( $id, $reassign );
if ( ! $result ) {
return new WP_Error(
'rest_cannot_delete',
__( 'The user cannot be deleted.' ),
array( 'status' => 500 )
);
}
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
/**
* Fires immediately after a user is deleted via the REST API.
*
* @since 4.7.0
*
* @param WP_User $user The user data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_user', $user, $response, $request );
return $response;
}
/**
* Checks if a given request has access to delete the current user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
*/
public function delete_current_item_permissions_check( $request ) {
$request['id'] = get_current_user_id();
return $this->delete_item_permissions_check( $request );
}
/**
* Deletes the current user.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_current_item( $request ) {
$request['id'] = get_current_user_id();
return $this->delete_item( $request );
}
/**
* Prepares a single user output for response.
*
* @since 4.7.0
* @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param WP_User $item User object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$user = $item;
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'id', $fields, true ) ) {
$data['id'] = $user->ID;
}
if ( in_array( 'username', $fields, true ) ) {
$data['username'] = $user->user_login;
}
if ( in_array( 'name', $fields, true ) ) {
$data['name'] = $user->display_name;
}
if ( in_array( 'first_name', $fields, true ) ) {
$data['first_name'] = $user->first_name;
}
if ( in_array( 'last_name', $fields, true ) ) {
$data['last_name'] = $user->last_name;
}
if ( in_array( 'email', $fields, true ) ) {
$data['email'] = $user->user_email;
}
if ( in_array( 'url', $fields, true ) ) {
$data['url'] = $user->user_url;
}
if ( in_array( 'description', $fields, true ) ) {
$data['description'] = $user->description;
}
if ( in_array( 'link', $fields, true ) ) {
$data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
}
if ( in_array( 'locale', $fields, true ) ) {
$data['locale'] = get_user_locale( $user );
}
if ( in_array( 'nickname', $fields, true ) ) {
$data['nickname'] = $user->nickname;
}
if ( in_array( 'slug', $fields, true ) ) {
$data['slug'] = $user->user_nicename;
}
if ( in_array( 'roles', $fields, true ) ) {
// Defensively call array_values() to ensure an array is returned.
$data['roles'] = array_values( $user->roles );
}
if ( in_array( 'registered_date', $fields, true ) ) {
$data['registered_date'] = gmdate( 'c', strtotime( $user->user_registered ) );
}
if ( in_array( 'capabilities', $fields, true ) ) {
$data['capabilities'] = (object) $user->allcaps;
}
if ( in_array( 'extra_capabilities', $fields, true ) ) {
$data['extra_capabilities'] = (object) $user->caps;
}
if ( in_array( 'avatar_urls', $fields, true ) ) {
$data['avatar_urls'] = rest_get_avatar_urls( $user );
}
if ( in_array( 'meta', $fields, true ) ) {
$data['meta'] = $this->meta->get_value( $user->ID, $request );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'embed';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $user ) );
}
/**
* Filters user data returned from the REST API.
*
* @since 4.7.0
*
* @param WP_REST_Response $response The response object.
* @param WP_User $user User object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_prepare_user', $response, $user, $request );
}
/**
* Prepares links for the user request.
*
* @since 4.7.0
*
* @param WP_User $user User object.
* @return array Links for the given user.
*/
protected function prepare_links( $user ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Prepares a single user for creation or update.
*
* @since 4.7.0
*
* @param WP_REST_Request $request Request object.
* @return object User object.
*/
protected function prepare_item_for_database( $request ) {
$prepared_user = new stdClass();
$schema = $this->get_item_schema();
// Required arguments.
if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
$prepared_user->user_email = $request['email'];
}
if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
$prepared_user->user_login = $request['username'];
}
if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
$prepared_user->user_pass = $request['password'];
}
// Optional arguments.
if ( isset( $request['id'] ) ) {
$prepared_user->ID = absint( $request['id'] );
}
if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
$prepared_user->display_name = $request['name'];
}
if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
$prepared_user->first_name = $request['first_name'];
}
if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
$prepared_user->last_name = $request['last_name'];
}
if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
$prepared_user->nickname = $request['nickname'];
}
if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
$prepared_user->user_nicename = $request['slug'];
}
if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
$prepared_user->description = $request['description'];
}
if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
$prepared_user->user_url = $request['url'];
}
if ( isset( $request['locale'] ) && ! empty( $schema['properties']['locale'] ) ) {
$prepared_user->locale = $request['locale'];
}
// Setting roles will be handled outside of this function.
if ( isset( $request['roles'] ) ) {
$prepared_user->role = false;
}
/**
* Filters user data before insertion via the REST API.
*
* @since 4.7.0
*
* @param object $prepared_user User object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
}
/**
* Determines if the current user is allowed to make the desired roles change.
*
* @since 4.7.0
*
* @global WP_Roles $wp_roles WordPress role management object.
*
* @param int $user_id User ID.
* @param array $roles New user roles.
* @return true|WP_Error True if the current user is allowed to make the role change,
* otherwise a WP_Error object.
*/
protected function check_role_update( $user_id, $roles ) {
global $wp_roles;
foreach ( $roles as $role ) {
if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
return new WP_Error(
'rest_user_invalid_role',
/* translators: %s: Role key. */
sprintf( __( 'The role %s does not exist.' ), $role ),
array( 'status' => 400 )
);
}
$potential_role = $wp_roles->role_objects[ $role ];
/*
* Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
* Multisite super admins can freely edit their blog roles -- they possess all caps.
*/
if ( ! ( is_multisite()
&& current_user_can( 'manage_sites' ) )
&& get_current_user_id() === $user_id
&& ! $potential_role->has_cap( 'edit_users' )
) {
return new WP_Error(
'rest_user_invalid_role',
__( 'Sorry, you are not allowed to give users that role.' ),
array( 'status' => rest_authorization_required_code() )
);
}
// Include user admin functions to get access to get_editable_roles().
require_once ABSPATH . 'wp-admin/includes/user.php';
// The new role must be editable by the logged-in user.
$editable_roles = get_editable_roles();
if ( empty( $editable_roles[ $role ] ) ) {
return new WP_Error(
'rest_user_invalid_role',
__( 'Sorry, you are not allowed to give users that role.' ),
array( 'status' => 403 )
);
}
}
return true;
}
/**
* Check a username for the REST API.
*
* Performs a couple of checks like edit_user() in wp-admin/includes/user.php.
*
* @since 4.7.0
*
* @param string $value The username submitted in the request.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter name.
* @return string|WP_Error The sanitized username, if valid, otherwise an error.
*/
public function check_username( $value, $request, $param ) {
$username = (string) $value;
if ( ! validate_username( $username ) ) {
return new WP_Error(
'rest_user_invalid_username',
__( 'This username is invalid because it uses illegal characters. Please enter a valid username.' ),
array( 'status' => 400 )
);
}
/** This filter is documented in wp-includes/user.php */
$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );
if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) {
return new WP_Error(
'rest_user_invalid_username',
__( 'Sorry, that username is not allowed.' ),
array( 'status' => 400 )
);
}
return $username;
}
/**
* Check a user password for the REST API.
*
* Performs a couple of checks like edit_user() in wp-admin/includes/user.php.
*
* @since 4.7.0
*
* @param string $value The password submitted in the request.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter name.
* @return string|WP_Error The sanitized password, if valid, otherwise an error.
*/
public function check_user_password( $value, $request, $param ) {
$password = (string) $value;
if ( empty( $password ) ) {
return new WP_Error(
'rest_user_invalid_password',
__( 'Passwords cannot be empty.' ),
array( 'status' => 400 )
);
}
if ( str_contains( $password, '\\' ) ) {
return new WP_Error(
'rest_user_invalid_password',
sprintf(
/* translators: %s: The '\' character. */
__( 'Passwords cannot contain the "%s" character.' ),
'\\'
),
array( 'status' => 400 )
);
}
return $password;
}
/**
* Retrieves the user's schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'user',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the user.' ),
'type' => 'integer',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'username' => array(
'description' => __( 'Login name for the user.' ),
'type' => 'string',
'context' => array( 'edit' ),
'required' => true,
'arg_options' => array(
'sanitize_callback' => array( $this, 'check_username' ),
),
),
'name' => array(
'description' => __( 'Display name for the user.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'first_name' => array(
'description' => __( 'First name for the user.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'last_name' => array(
'description' => __( 'Last name for the user.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'email' => array(
'description' => __( 'The email address for the user.' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'edit' ),
'required' => true,
),
'url' => array(
'description' => __( 'URL of the user.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Description of the user.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
),
'link' => array(
'description' => __( 'Author URL of the user.' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'locale' => array(
'description' => __( 'Locale for the user.' ),
'type' => 'string',
'enum' => array_merge( array( '', 'en_US' ), get_available_languages() ),
'context' => array( 'edit' ),
),
'nickname' => array(
'description' => __( 'The nickname for the user.' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the user.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => array( $this, 'sanitize_slug' ),
),
),
'registered_date' => array(
'description' => __( 'Registration date for the user.' ),
'type' => 'string',
'format' => 'date-time',
'context' => array( 'edit' ),
'readonly' => true,
),
'roles' => array(
'description' => __( 'Roles assigned to the user.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'edit' ),
),
'password' => array(
'description' => __( 'Password for the user (never included).' ),
'type' => 'string',
'context' => array(), // Password is never displayed.
'required' => true,
'arg_options' => array(
'sanitize_callback' => array( $this, 'check_user_password' ),
),
),
'capabilities' => array(
'description' => __( 'All capabilities assigned to the user.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
'extra_capabilities' => array(
'description' => __( 'Any extra capabilities assigned to the user.' ),
'type' => 'object',
'context' => array( 'edit' ),
'readonly' => true,
),
),
);
if ( get_option( 'show_avatars' ) ) {
$avatar_properties = array();
$avatar_sizes = rest_get_avatar_sizes();
foreach ( $avatar_sizes as $size ) {
$avatar_properties[ $size ] = array(
/* translators: %d: Avatar image size in pixels. */
'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$schema['properties']['avatar_urls'] = array(
'description' => __( 'Avatar URLs for the user.' ),
'type' => 'object',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
'properties' => $avatar_properties,
);
}
$schema['properties']['meta'] = $this->meta->get_field_schema();
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* Retrieves the query params for collections.
*
* @since 4.7.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
$query_params = parent::get_collection_params();
$query_params['context']['default'] = 'view';
$query_params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['include'] = array(
'description' => __( 'Limit result set to specific IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.' ),
'type' => 'integer',
);
$query_params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.' ),
'enum' => array( 'asc', 'desc' ),
'type' => 'string',
);
$query_params['orderby'] = array(
'default' => 'name',
'description' => __( 'Sort collection by user attribute.' ),
'enum' => array(
'id',
'include',
'name',
'registered_date',
'slug',
'include_slugs',
'email',
'url',
),
'type' => 'string',
);
$query_params['slug'] = array(
'description' => __( 'Limit result set to users with one or more specific slugs.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
$query_params['roles'] = array(
'description' => __( 'Limit result set to users matching at least one specific role provided. Accepts csv list or single role.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
$query_params['capabilities'] = array(
'description' => __( 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
);
$query_params['who'] = array(
'description' => __( 'Limit result set to users who are considered authors.' ),
'type' => 'string',
'enum' => array(
'authors',
),
);
$query_params['has_published_posts'] = array(
'description' => __( 'Limit result set to users who have published posts.' ),
'type' => array( 'boolean', 'array' ),
'items' => array(
'type' => 'string',
'enum' => get_post_types( array( 'show_in_rest' => true ), 'names' ),
),
);
/**
* Filters REST API collection parameters for the users controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_User_Query parameter. Use the
* `rest_user_query` filter to set WP_User_Query arguments.
*
* @since 4.7.0
*
* @param array $query_params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'rest_user_collection_params', $query_params );
}
}
endpoints/class-wp-rest-widget-types-controller.php 0000644 00000044475 15153067731 0016616 0 ustar 00 <?php
/**
* REST API: WP_REST_Widget_Types_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Core class to access widget types via the REST API.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Widget_Types_Controller extends WP_REST_Controller {
/**
* Constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'widget-types';
}
/**
* Registers the widget type routes.
*
* @since 5.8.0
*
* @see register_rest_route()
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'The widget type id.' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/encode',
array(
'args' => array(
'id' => array(
'description' => __( 'The widget type id.' ),
'type' => 'string',
'required' => true,
),
'instance' => array(
'description' => __( 'Current instance settings of the widget.' ),
'type' => 'object',
),
'form_data' => array(
'description' => __( 'Serialized widget form data to encode into instance settings.' ),
'type' => 'string',
'sanitize_callback' => static function ( $form_data ) {
$array = array();
wp_parse_str( $form_data, $array );
return $array;
},
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'callback' => array( $this, 'encode_form_data' ),
),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/render',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'callback' => array( $this, 'render' ),
'args' => array(
'id' => array(
'description' => __( 'The widget type id.' ),
'type' => 'string',
'required' => true,
),
'instance' => array(
'description' => __( 'Current instance settings of the widget.' ),
'type' => 'object',
),
),
),
)
);
}
/**
* Checks whether a given request has permission to read widget types.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
return $this->check_read_permission();
}
/**
* Retrieves the list of all widget types.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$data = array();
foreach ( $this->get_widgets() as $widget ) {
$widget_type = $this->prepare_item_for_response( $widget, $request );
$data[] = $this->prepare_response_for_collection( $widget_type );
}
return rest_ensure_response( $data );
}
/**
* Checks if a given request has access to read a widget type.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$check = $this->check_read_permission();
if ( is_wp_error( $check ) ) {
return $check;
}
$widget_id = $request['id'];
$widget_type = $this->get_widget( $widget_id );
if ( is_wp_error( $widget_type ) ) {
return $widget_type;
}
return true;
}
/**
* Checks whether the user can read widget types.
*
* @since 5.8.0
*
* @return true|WP_Error True if the widget type is visible, WP_Error otherwise.
*/
protected function check_read_permission() {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Gets the details about the requested widget.
*
* @since 5.8.0
*
* @param string $id The widget type id.
* @return array|WP_Error The array of widget data if the name is valid, WP_Error otherwise.
*/
public function get_widget( $id ) {
foreach ( $this->get_widgets() as $widget ) {
if ( $id === $widget['id'] ) {
return $widget;
}
}
return new WP_Error( 'rest_widget_type_invalid', __( 'Invalid widget type.' ), array( 'status' => 404 ) );
}
/**
* Normalize array of widgets.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
* @global array $wp_registered_widgets The list of registered widgets.
*
* @return array Array of widgets.
*/
protected function get_widgets() {
global $wp_widget_factory, $wp_registered_widgets;
$widgets = array();
foreach ( $wp_registered_widgets as $widget ) {
$parsed_id = wp_parse_widget_id( $widget['id'] );
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
$widget['id'] = $parsed_id['id_base'];
$widget['is_multi'] = (bool) $widget_object;
if ( isset( $widget['name'] ) ) {
$widget['name'] = html_entity_decode( $widget['name'], ENT_QUOTES, get_bloginfo( 'charset' ) );
}
if ( isset( $widget['description'] ) ) {
$widget['description'] = html_entity_decode( $widget['description'], ENT_QUOTES, get_bloginfo( 'charset' ) );
}
unset( $widget['callback'] );
$classname = '';
foreach ( (array) $widget['classname'] as $cn ) {
if ( is_string( $cn ) ) {
$classname .= '_' . $cn;
} elseif ( is_object( $cn ) ) {
$classname .= '_' . get_class( $cn );
}
}
$widget['classname'] = ltrim( $classname, '_' );
$widgets[ $widget['id'] ] = $widget;
}
ksort( $widgets );
return $widgets;
}
/**
* Retrieves a single widget type from the collection.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$widget_id = $request['id'];
$widget_type = $this->get_widget( $widget_id );
if ( is_wp_error( $widget_type ) ) {
return $widget_type;
}
$data = $this->prepare_item_for_response( $widget_type, $request );
return rest_ensure_response( $data );
}
/**
* Prepares a widget type object for serialization.
*
* @since 5.8.0
* @since 5.9.0 Renamed `$widget_type` to `$item` to match parent class for PHP 8 named parameter support.
*
* @param array $item Widget type data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response Widget type data.
*/
public function prepare_item_for_response( $item, $request ) {
// Restores the more descriptive, specific name for use within this method.
$widget_type = $item;
$fields = $this->get_fields_for_response( $request );
$data = array(
'id' => $widget_type['id'],
);
$schema = $this->get_item_schema();
$extra_fields = array(
'name',
'description',
'is_multi',
'classname',
'widget_class',
'option_name',
'customize_selective_refresh',
);
foreach ( $extra_fields as $extra_field ) {
if ( ! rest_is_field_included( $extra_field, $fields ) ) {
continue;
}
if ( isset( $widget_type[ $extra_field ] ) ) {
$field = $widget_type[ $extra_field ];
} elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
$field = $schema['properties'][ $extra_field ]['default'];
} else {
$field = '';
}
$data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $widget_type ) );
}
/**
* Filters the REST API response for a widget type.
*
* @since 5.8.0
*
* @param WP_REST_Response $response The response object.
* @param array $widget_type The array of widget data.
* @param WP_REST_Request $request The request object.
*/
return apply_filters( 'rest_prepare_widget_type', $response, $widget_type, $request );
}
/**
* Prepares links for the widget type.
*
* @since 5.8.0
*
* @param array $widget_type Widget type data.
* @return array Links for the given widget type.
*/
protected function prepare_links( $widget_type ) {
return array(
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $widget_type['id'] ) ),
),
);
}
/**
* Retrieves the widget type's schema, conforming to JSON Schema.
*
* @since 5.8.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'widget-type',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique slug identifying the widget type.' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Human-readable name identifying the widget type.' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the widget.' ),
'type' => 'string',
'default' => '',
'context' => array( 'view', 'edit', 'embed' ),
),
'is_multi' => array(
'description' => __( 'Whether the widget supports multiple instances' ),
'type' => 'boolean',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'classname' => array(
'description' => __( 'Class name' ),
'type' => 'string',
'default' => '',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
),
);
$this->schema = $schema;
return $this->add_additional_fields_schema( $this->schema );
}
/**
* An RPC-style endpoint which can be used by clients to turn user input in
* a widget admin form into an encoded instance object.
*
* Accepts:
*
* - id: A widget type ID.
* - instance: A widget's encoded instance object. Optional.
* - form_data: Form data from submitting a widget's admin form. Optional.
*
* Returns:
* - instance: The encoded instance object after updating the widget with
* the given form data.
* - form: The widget's admin form after updating the widget with the
* given form data.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function encode_form_data( $request ) {
global $wp_widget_factory;
$id = $request['id'];
$widget_object = $wp_widget_factory->get_widget_object( $id );
if ( ! $widget_object ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Cannot preview a widget that does not extend WP_Widget.' ),
array( 'status' => 400 )
);
}
/*
* Set the widget's number so that the id attributes in the HTML that we
* return are predictable.
*/
if ( isset( $request['number'] ) && is_numeric( $request['number'] ) ) {
$widget_object->_set( (int) $request['number'] );
} else {
$widget_object->_set( -1 );
}
if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
$serialized_instance = base64_decode( $request['instance']['encoded'] );
if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is malformed.' ),
array( 'status' => 400 )
);
}
$instance = unserialize( $serialized_instance );
} else {
$instance = array();
}
if (
isset( $request['form_data'][ "widget-$id" ] ) &&
is_array( $request['form_data'][ "widget-$id" ] )
) {
$new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0];
$old_instance = $instance;
$instance = $widget_object->update( $new_instance, $old_instance );
/** This filter is documented in wp-includes/class-wp-widget.php */
$instance = apply_filters(
'widget_update_callback',
$instance,
$new_instance,
$old_instance,
$widget_object
);
}
$serialized_instance = serialize( $instance );
$widget_key = $wp_widget_factory->get_widget_key( $id );
$response = array(
'form' => trim(
$this->get_widget_form(
$widget_object,
$instance
)
),
'preview' => trim(
$this->get_widget_preview(
$widget_key,
$instance
)
),
'instance' => array(
'encoded' => base64_encode( $serialized_instance ),
'hash' => wp_hash( $serialized_instance ),
),
);
if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
// Use new stdClass so that JSON result is {} and not [].
$response['instance']['raw'] = empty( $instance ) ? new stdClass() : $instance;
}
return rest_ensure_response( $response );
}
/**
* Returns the output of WP_Widget::widget() when called with the provided
* instance. Used by encode_form_data() to preview a widget.
* @since 5.8.0
*
* @param string $widget The widget's PHP class name (see class-wp-widget.php).
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_preview( $widget, $instance ) {
ob_start();
the_widget( $widget, $instance );
return ob_get_clean();
}
/**
* Returns the output of WP_Widget::form() when called with the provided
* instance. Used by encode_form_data() to preview a widget's form.
*
* @since 5.8.0
*
* @param WP_Widget $widget_object Widget object to call widget() on.
* @param array $instance Widget instance settings.
* @return string
*/
private function get_widget_form( $widget_object, $instance ) {
ob_start();
/** This filter is documented in wp-includes/class-wp-widget.php */
$instance = apply_filters(
'widget_form_callback',
$instance,
$widget_object
);
if ( false !== $instance ) {
$return = $widget_object->form( $instance );
/** This filter is documented in wp-includes/class-wp-widget.php */
do_action_ref_array(
'in_widget_form',
array( &$widget_object, &$return, $instance )
);
}
return ob_get_clean();
}
/**
* Renders a single Legacy Widget and wraps it in a JSON-encodable array.
*
* @since 5.9.0
*
* @param WP_REST_Request $request Full details about the request.
*
* @return array An array with rendered Legacy Widget HTML.
*/
public function render( $request ) {
return array(
'preview' => $this->render_legacy_widget_preview_iframe(
$request['id'],
isset( $request['instance'] ) ? $request['instance'] : null
),
);
}
/**
* Renders a page containing a preview of the requested Legacy Widget block.
*
* @since 5.9.0
*
* @param string $id_base The id base of the requested widget.
* @param array $instance The widget instance attributes.
*
* @return string Rendered Legacy Widget block preview.
*/
private function render_legacy_widget_preview_iframe( $id_base, $instance ) {
if ( ! defined( 'IFRAME_REQUEST' ) ) {
define( 'IFRAME_REQUEST', true );
}
ob_start();
?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="profile" href="https://gmpg.org/xfn/11" />
<?php wp_head(); ?>
<style>
/* Reset theme styles */
html, body, #page, #content {
padding: 0 !important;
margin: 0 !important;
}
</style>
</head>
<body <?php body_class(); ?>>
<div id="page" class="site">
<div id="content" class="site-content">
<?php
$registry = WP_Block_Type_Registry::get_instance();
$block = $registry->get_registered( 'core/legacy-widget' );
echo $block->render(
array(
'idBase' => $id_base,
'instance' => $instance,
)
);
?>
</div><!-- #content -->
</div><!-- #page -->
<?php wp_footer(); ?>
</body>
</html>
<?php
return ob_get_clean();
}
/**
* Retrieves the query params for collections.
*
* @since 5.8.0
*
* @return array Collection parameters.
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
endpoints/class-wp-rest-widgets-controller.php 0000644 00000063556 15153067731 0015640 0 ustar 00 <?php
/**
* REST API: WP_REST_Widgets_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.8.0
*/
/**
* Core class to access widgets via the REST API.
*
* @since 5.8.0
*
* @see WP_REST_Controller
*/
class WP_REST_Widgets_Controller extends WP_REST_Controller {
/**
* Tracks whether {@see retrieve_widgets()} has been called in the current request.
*
* @since 5.9.0
* @var bool
*/
protected $widgets_retrieved = false;
/**
* Whether the controller supports batching.
*
* @since 5.9.0
* @var array
*/
protected $allow_batch = array( 'v1' => true );
/**
* Widgets controller constructor.
*
* @since 5.8.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'widgets';
}
/**
* Registers the widget routes for the controller.
*
* @since 5.8.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
$this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema(),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
$this->rest_base . '/(?P<id>[\w\-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'description' => __( 'Whether to force removal of the widget, or move it to the inactive sidebar.' ),
'type' => 'boolean',
),
),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Checks if a given request has access to get widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
$this->retrieve_widgets();
if ( isset( $request['sidebar'] ) && $this->check_read_sidebar_permission( $request['sidebar'] ) ) {
return true;
}
foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
if ( $this->check_read_sidebar_permission( $sidebar_id ) ) {
return true;
}
}
return $this->permissions_check( $request );
}
/**
* Retrieves a collection of widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
$this->retrieve_widgets();
$prepared = array();
$permissions_check = $this->permissions_check( $request );
foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
if ( isset( $request['sidebar'] ) && $sidebar_id !== $request['sidebar'] ) {
continue;
}
if ( is_wp_error( $permissions_check ) && ! $this->check_read_sidebar_permission( $sidebar_id ) ) {
continue;
}
foreach ( $widget_ids as $widget_id ) {
$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
if ( ! is_wp_error( $response ) ) {
$prepared[] = $this->prepare_response_for_collection( $response );
}
}
}
return new WP_REST_Response( $prepared );
}
/**
* Checks if a given request has access to get a widget.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) {
$this->retrieve_widgets();
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
if ( $sidebar_id && $this->check_read_sidebar_permission( $sidebar_id ) ) {
return true;
}
return $this->permissions_check( $request );
}
/**
* Checks if a sidebar can be read publicly.
*
* @since 5.9.0
*
* @param string $sidebar_id The sidebar ID.
* @return bool Whether the sidebar can be read.
*/
protected function check_read_sidebar_permission( $sidebar_id ) {
$sidebar = wp_get_sidebar( $sidebar_id );
return ! empty( $sidebar['show_in_rest'] );
}
/**
* Gets an individual widget.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_item( $request ) {
$this->retrieve_widgets();
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
if ( is_null( $sidebar_id ) ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
}
/**
* Checks if a given request has access to create widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function create_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Creates a widget.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item( $request ) {
$sidebar_id = $request['sidebar'];
$widget_id = $this->save_widget( $request, $sidebar_id );
if ( is_wp_error( $widget_id ) ) {
return $widget_id;
}
wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );
$request['context'] = 'edit';
$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$response->set_status( 201 );
return $response;
}
/**
* Checks if a given request has access to update widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function update_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Updates an existing widget.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function update_item( $request ) {
global $wp_widget_factory;
/*
* retrieve_widgets() contains logic to move "hidden" or "lost" widgets to the
* wp_inactive_widgets sidebar based on the contents of the $sidebars_widgets global.
*
* When batch requests are processed, this global is not properly updated by previous
* calls, resulting in widgets incorrectly being moved to the wp_inactive_widgets
* sidebar.
*
* See https://core.trac.wordpress.org/ticket/53657.
*/
wp_get_sidebars_widgets();
$this->retrieve_widgets();
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
// Allow sidebar to be unset or missing when widget is not a WP_Widget.
$parsed_id = wp_parse_widget_id( $widget_id );
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
if ( is_null( $sidebar_id ) && $widget_object ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
if (
$request->has_param( 'instance' ) ||
$request->has_param( 'form_data' )
) {
$maybe_error = $this->save_widget( $request, $sidebar_id );
if ( is_wp_error( $maybe_error ) ) {
return $maybe_error;
}
}
if ( $request->has_param( 'sidebar' ) ) {
if ( $sidebar_id !== $request['sidebar'] ) {
$sidebar_id = $request['sidebar'];
wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );
}
}
$request['context'] = 'edit';
return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
}
/**
* Checks if a given request has access to delete widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function delete_item_permissions_check( $request ) {
return $this->permissions_check( $request );
}
/**
* Deletes a widget.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
* @global array $wp_registered_widget_updates The registered widget update functions.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function delete_item( $request ) {
global $wp_widget_factory, $wp_registered_widget_updates;
/*
* retrieve_widgets() contains logic to move "hidden" or "lost" widgets to the
* wp_inactive_widgets sidebar based on the contents of the $sidebars_widgets global.
*
* When batch requests are processed, this global is not properly updated by previous
* calls, resulting in widgets incorrectly being moved to the wp_inactive_widgets
* sidebar.
*
* See https://core.trac.wordpress.org/ticket/53657.
*/
wp_get_sidebars_widgets();
$this->retrieve_widgets();
$widget_id = $request['id'];
$sidebar_id = wp_find_widgets_sidebar( $widget_id );
if ( is_null( $sidebar_id ) ) {
return new WP_Error(
'rest_widget_not_found',
__( 'No widget was found with that id.' ),
array( 'status' => 404 )
);
}
$request['context'] = 'edit';
if ( $request['force'] ) {
$response = $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
$parsed_id = wp_parse_widget_id( $widget_id );
$id_base = $parsed_id['id_base'];
$original_post = $_POST;
$original_request = $_REQUEST;
$_POST = array(
'sidebar' => $sidebar_id,
"widget-$id_base" => array(),
'the-widget-id' => $widget_id,
'delete_widget' => '1',
);
$_REQUEST = $_POST;
/** This action is documented in wp-admin/widgets-form.php */
do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base );
$callback = $wp_registered_widget_updates[ $id_base ]['callback'];
$params = $wp_registered_widget_updates[ $id_base ]['params'];
if ( is_callable( $callback ) ) {
ob_start();
call_user_func_array( $callback, $params );
ob_end_clean();
}
$_POST = $original_post;
$_REQUEST = $original_request;
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
if ( $widget_object ) {
/*
* WP_Widget sets `updated = true` after an update to prevent more than one widget
* from being saved per request. This isn't what we want in the REST API, though,
* as we support batch requests.
*/
$widget_object->updated = false;
}
wp_assign_widget_to_sidebar( $widget_id, '' );
$response->set_data(
array(
'deleted' => true,
'previous' => $response->get_data(),
)
);
} else {
wp_assign_widget_to_sidebar( $widget_id, 'wp_inactive_widgets' );
$response = $this->prepare_item_for_response(
array(
'sidebar_id' => 'wp_inactive_widgets',
'widget_id' => $widget_id,
),
$request
);
}
/**
* Fires after a widget is deleted via the REST API.
*
* @since 5.8.0
*
* @param string $widget_id ID of the widget marked for deletion.
* @param string $sidebar_id ID of the sidebar the widget was deleted from.
* @param WP_REST_Response|WP_Error $response The response data, or WP_Error object on failure.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_widget', $widget_id, $sidebar_id, $response, $request );
return $response;
}
/**
* Performs a permissions check for managing widgets.
*
* @since 5.8.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error
*/
protected function permissions_check( $request ) {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return new WP_Error(
'rest_cannot_manage_widgets',
__( 'Sorry, you are not allowed to manage widgets on this site.' ),
array(
'status' => rest_authorization_required_code(),
)
);
}
return true;
}
/**
* Looks for "lost" widgets once per request.
*
* @since 5.9.0
*
* @see retrieve_widgets()
*/
protected function retrieve_widgets() {
if ( ! $this->widgets_retrieved ) {
retrieve_widgets();
$this->widgets_retrieved = true;
}
}
/**
* Saves the widget in the request object.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
* @global array $wp_registered_widget_updates The registered widget update functions.
*
* @param WP_REST_Request $request Full details about the request.
* @param string $sidebar_id ID of the sidebar the widget belongs to.
* @return string|WP_Error The saved widget ID.
*/
protected function save_widget( $request, $sidebar_id ) {
global $wp_widget_factory, $wp_registered_widget_updates;
require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number().
if ( isset( $request['id'] ) ) {
// Saving an existing widget.
$id = $request['id'];
$parsed_id = wp_parse_widget_id( $id );
$id_base = $parsed_id['id_base'];
$number = isset( $parsed_id['number'] ) ? $parsed_id['number'] : null;
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
$creating = false;
} elseif ( $request['id_base'] ) {
// Saving a new widget.
$id_base = $request['id_base'];
$widget_object = $wp_widget_factory->get_widget_object( $id_base );
$number = $widget_object ? next_widget_id_number( $id_base ) : null;
$id = $widget_object ? $id_base . '-' . $number : $id_base;
$creating = true;
} else {
return new WP_Error(
'rest_invalid_widget',
__( 'Widget type (id_base) is required.' ),
array( 'status' => 400 )
);
}
if ( ! isset( $wp_registered_widget_updates[ $id_base ] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided widget type (id_base) cannot be updated.' ),
array( 'status' => 400 )
);
}
if ( isset( $request['instance'] ) ) {
if ( ! $widget_object ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Cannot set instance on a widget that does not extend WP_Widget.' ),
array( 'status' => 400 )
);
}
if ( isset( $request['instance']['raw'] ) ) {
if ( empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'Widget type does not support raw instances.' ),
array( 'status' => 400 )
);
}
$instance = $request['instance']['raw'];
} elseif ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
$serialized_instance = base64_decode( $request['instance']['encoded'] );
if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is malformed.' ),
array( 'status' => 400 )
);
}
$instance = unserialize( $serialized_instance );
} else {
return new WP_Error(
'rest_invalid_widget',
__( 'The provided instance is invalid. Must contain raw OR encoded and hash.' ),
array( 'status' => 400 )
);
}
$form_data = array(
"widget-$id_base" => array(
$number => $instance,
),
'sidebar' => $sidebar_id,
);
} elseif ( isset( $request['form_data'] ) ) {
$form_data = $request['form_data'];
} else {
$form_data = array();
}
$original_post = $_POST;
$original_request = $_REQUEST;
foreach ( $form_data as $key => $value ) {
$slashed_value = wp_slash( $value );
$_POST[ $key ] = $slashed_value;
$_REQUEST[ $key ] = $slashed_value;
}
$callback = $wp_registered_widget_updates[ $id_base ]['callback'];
$params = $wp_registered_widget_updates[ $id_base ]['params'];
if ( is_callable( $callback ) ) {
ob_start();
call_user_func_array( $callback, $params );
ob_end_clean();
}
$_POST = $original_post;
$_REQUEST = $original_request;
if ( $widget_object ) {
// Register any multi-widget that the update callback just created.
$widget_object->_set( $number );
$widget_object->_register_one( $number );
/*
* WP_Widget sets `updated = true` after an update to prevent more than one widget
* from being saved per request. This isn't what we want in the REST API, though,
* as we support batch requests.
*/
$widget_object->updated = false;
}
/**
* Fires after a widget is created or updated via the REST API.
*
* @since 5.8.0
*
* @param string $id ID of the widget being saved.
* @param string $sidebar_id ID of the sidebar containing the widget being saved.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a widget, false when updating.
*/
do_action( 'rest_after_save_widget', $id, $sidebar_id, $request, $creating );
return $id;
}
/**
* Prepares the widget for the REST response.
*
* @since 5.8.0
*
* @global WP_Widget_Factory $wp_widget_factory
* @global array $wp_registered_widgets The registered widgets.
*
* @param array $item An array containing a widget_id and sidebar_id.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
global $wp_widget_factory, $wp_registered_widgets;
$widget_id = $item['widget_id'];
$sidebar_id = $item['sidebar_id'];
if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
return new WP_Error(
'rest_invalid_widget',
__( 'The requested widget is invalid.' ),
array( 'status' => 500 )
);
}
$widget = $wp_registered_widgets[ $widget_id ];
$parsed_id = wp_parse_widget_id( $widget_id );
$fields = $this->get_fields_for_response( $request );
$prepared = array(
'id' => $widget_id,
'id_base' => $parsed_id['id_base'],
'sidebar' => $sidebar_id,
'rendered' => '',
'rendered_form' => null,
'instance' => null,
);
if (
rest_is_field_included( 'rendered', $fields ) &&
'wp_inactive_widgets' !== $sidebar_id
) {
$prepared['rendered'] = trim( wp_render_widget( $widget_id, $sidebar_id ) );
}
if ( rest_is_field_included( 'rendered_form', $fields ) ) {
$rendered_form = wp_render_widget_control( $widget_id );
if ( ! is_null( $rendered_form ) ) {
$prepared['rendered_form'] = trim( $rendered_form );
}
}
if ( rest_is_field_included( 'instance', $fields ) ) {
$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
if ( $widget_object && isset( $parsed_id['number'] ) ) {
$all_instances = $widget_object->get_settings();
$instance = $all_instances[ $parsed_id['number'] ];
$serialized_instance = serialize( $instance );
$prepared['instance']['encoded'] = base64_encode( $serialized_instance );
$prepared['instance']['hash'] = wp_hash( $serialized_instance );
if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
// Use new stdClass so that JSON result is {} and not [].
$prepared['instance']['raw'] = empty( $instance ) ? new stdClass() : $instance;
}
}
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$prepared = $this->add_additional_fields_to_object( $prepared, $request );
$prepared = $this->filter_response_by_context( $prepared, $context );
$response = rest_ensure_response( $prepared );
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_links( $this->prepare_links( $prepared ) );
}
/**
* Filters the REST API response for a widget.
*
* @since 5.8.0
*
* @param WP_REST_Response|WP_Error $response The response object, or WP_Error object on failure.
* @param array $widget The registered widget data.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'rest_prepare_widget', $response, $widget, $request );
}
/**
* Prepares links for the widget.
*
* @since 5.8.0
*
* @param array $prepared Widget.
* @return array Links for the given widget.
*/
protected function prepare_links( $prepared ) {
$id_base = ! empty( $prepared['id_base'] ) ? $prepared['id_base'] : $prepared['id'];
return array(
'self' => array(
'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $prepared['id'] ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
'about' => array(
'href' => rest_url( sprintf( 'wp/v2/widget-types/%s', $id_base ) ),
'embeddable' => true,
),
'https://api.w.org/sidebar' => array(
'href' => rest_url( sprintf( 'wp/v2/sidebars/%s/', $prepared['sidebar'] ) ),
),
);
}
/**
* Gets the list of collection params.
*
* @since 5.8.0
*
* @return array[]
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'sidebar' => array(
'description' => __( 'The sidebar to return widgets for.' ),
'type' => 'string',
),
);
}
/**
* Retrieves the widget's schema, conforming to JSON Schema.
*
* @since 5.8.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'widget',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the widget.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'id_base' => array(
'description' => __( 'The type of the widget. Corresponds to ID in widget-types endpoint.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
),
'sidebar' => array(
'description' => __( 'The sidebar the widget belongs to.' ),
'type' => 'string',
'default' => 'wp_inactive_widgets',
'required' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'rendered' => array(
'description' => __( 'HTML representation of the widget.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'rendered_form' => array(
'description' => __( 'HTML representation of the widget admin form.' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
),
'instance' => array(
'description' => __( 'Instance settings of the widget, if supported.' ),
'type' => 'object',
'context' => array( 'edit' ),
'default' => null,
'properties' => array(
'encoded' => array(
'description' => __( 'Base64 encoded representation of the instance settings.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'hash' => array(
'description' => __( 'Cryptographic hash of the instance settings.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'raw' => array(
'description' => __( 'Unencoded instance settings, if supported.' ),
'type' => 'object',
'context' => array( 'edit' ),
),
),
),
'form_data' => array(
'description' => __( 'URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.' ),
'type' => 'string',
'context' => array(),
'arg_options' => array(
'sanitize_callback' => static function ( $form_data ) {
$array = array();
wp_parse_str( $form_data, $array );
return $array;
},
),
),
),
);
return $this->add_additional_fields_schema( $this->schema );
}
}
fields/class-wp-rest-comment-meta-fields.php 0000644 00000001577 15153067731 0015101 0 ustar 00 <?php
/**
* REST API: WP_REST_Comment_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to manage comment meta via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Retrieves the comment type for comment meta.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'comment';
}
/**
* Retrieves the comment meta subtype.
*
* @since 4.9.8
*
* @return string 'comment' There are no subtypes.
*/
protected function get_meta_subtype() {
return 'comment';
}
/**
* Retrieves the type for register_rest_field() in the context of comments.
*
* @since 4.7.0
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return 'comment';
}
}
fields/class-wp-rest-meta-fields.php 0000644 00000043542 15153067731 0013437 0 ustar 00 <?php
/**
* REST API: WP_REST_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class to manage meta values for an object via the REST API.
*
* @since 4.7.0
*/
#[AllowDynamicProperties]
abstract class WP_REST_Meta_Fields {
/**
* Retrieves the object meta type.
*
* @since 4.7.0
*
* @return string One of 'post', 'comment', 'term', 'user', or anything
* else supported by `_get_meta_table()`.
*/
abstract protected function get_meta_type();
/**
* Retrieves the object meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return '';
}
/**
* Retrieves the object type for register_rest_field().
*
* @since 4.7.0
*
* @return string The REST field type, such as post type name, taxonomy name, 'comment', or `user`.
*/
abstract protected function get_rest_field_type();
/**
* Registers the meta field.
*
* @since 4.7.0
* @deprecated 5.6.0
*
* @see register_rest_field()
*/
public function register_field() {
_deprecated_function( __METHOD__, '5.6.0' );
register_rest_field(
$this->get_rest_field_type(),
'meta',
array(
'get_callback' => array( $this, 'get_value' ),
'update_callback' => array( $this, 'update_value' ),
'schema' => $this->get_field_schema(),
)
);
}
/**
* Retrieves the meta field value.
*
* @since 4.7.0
*
* @param int $object_id Object ID to fetch meta for.
* @param WP_REST_Request $request Full details about the request.
* @return array Array containing the meta values keyed by name.
*/
public function get_value( $object_id, $request ) {
$fields = $this->get_registered_fields();
$response = array();
foreach ( $fields as $meta_key => $args ) {
$name = $args['name'];
$all_values = get_metadata( $this->get_meta_type(), $object_id, $meta_key, false );
if ( $args['single'] ) {
if ( empty( $all_values ) ) {
$value = $args['schema']['default'];
} else {
$value = $all_values[0];
}
$value = $this->prepare_value_for_response( $value, $request, $args );
} else {
$value = array();
if ( is_array( $all_values ) ) {
foreach ( $all_values as $row ) {
$value[] = $this->prepare_value_for_response( $row, $request, $args );
}
}
}
$response[ $name ] = $value;
}
return $response;
}
/**
* Prepares a meta value for a response.
*
* This is required because some native types cannot be stored correctly
* in the database, such as booleans. We need to cast back to the relevant
* type before passing back to JSON.
*
* @since 4.7.0
*
* @param mixed $value Meta value to prepare.
* @param WP_REST_Request $request Current request object.
* @param array $args Options for the field.
* @return mixed Prepared value.
*/
protected function prepare_value_for_response( $value, $request, $args ) {
if ( ! empty( $args['prepare_callback'] ) ) {
$value = call_user_func( $args['prepare_callback'], $value, $request, $args );
}
return $value;
}
/**
* Updates meta values.
*
* @since 4.7.0
*
* @param array $meta Array of meta parsed from the request.
* @param int $object_id Object ID to fetch meta for.
* @return null|WP_Error Null on success, WP_Error object on failure.
*/
public function update_value( $meta, $object_id ) {
$fields = $this->get_registered_fields();
$error = new WP_Error();
foreach ( $fields as $meta_key => $args ) {
$name = $args['name'];
if ( ! array_key_exists( $name, $meta ) ) {
continue;
}
$value = $meta[ $name ];
/*
* A null value means reset the field, which is essentially deleting it
* from the database and then relying on the default value.
*
* Non-single meta can also be removed by passing an empty array.
*/
if ( is_null( $value ) || ( array() === $value && ! $args['single'] ) ) {
$args = $this->get_registered_fields()[ $meta_key ];
if ( $args['single'] ) {
$current = get_metadata( $this->get_meta_type(), $object_id, $meta_key, true );
if ( is_wp_error( rest_validate_value_from_schema( $current, $args['schema'] ) ) ) {
$error->add(
'rest_invalid_stored_value',
/* translators: %s: Custom field key. */
sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
array( 'status' => 500 )
);
continue;
}
}
$result = $this->delete_meta_value( $object_id, $meta_key, $name );
if ( is_wp_error( $result ) ) {
$error->merge_from( $result );
}
continue;
}
if ( ! $args['single'] && is_array( $value ) && count( array_filter( $value, 'is_null' ) ) ) {
$error->add(
'rest_invalid_stored_value',
/* translators: %s: Custom field key. */
sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
array( 'status' => 500 )
);
continue;
}
$is_valid = rest_validate_value_from_schema( $value, $args['schema'], 'meta.' . $name );
if ( is_wp_error( $is_valid ) ) {
$is_valid->add_data( array( 'status' => 400 ) );
$error->merge_from( $is_valid );
continue;
}
$value = rest_sanitize_value_from_schema( $value, $args['schema'] );
if ( $args['single'] ) {
$result = $this->update_meta_value( $object_id, $meta_key, $name, $value );
} else {
$result = $this->update_multi_meta_value( $object_id, $meta_key, $name, $value );
}
if ( is_wp_error( $result ) ) {
$error->merge_from( $result );
continue;
}
}
if ( $error->has_errors() ) {
return $error;
}
return null;
}
/**
* Deletes a meta value for an object.
*
* @since 4.7.0
*
* @param int $object_id Object ID the field belongs to.
* @param string $meta_key Key for the field.
* @param string $name Name for the field that is exposed in the REST API.
* @return true|WP_Error True if meta field is deleted, WP_Error otherwise.
*/
protected function delete_meta_value( $object_id, $meta_key, $name ) {
$meta_type = $this->get_meta_type();
if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_delete',
/* translators: %s: Custom field key. */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
if ( null === get_metadata_raw( $meta_type, $object_id, wp_slash( $meta_key ) ) ) {
return true;
}
if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ) ) ) {
return new WP_Error(
'rest_meta_database_error',
__( 'Could not delete meta value from database.' ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
return true;
}
/**
* Updates multiple meta values for an object.
*
* Alters the list of values in the database to match the list of provided values.
*
* @since 4.7.0
*
* @param int $object_id Object ID to update.
* @param string $meta_key Key for the custom field.
* @param string $name Name for the field that is exposed in the REST API.
* @param array $values List of values to update to.
* @return true|WP_Error True if meta fields are updated, WP_Error otherwise.
*/
protected function update_multi_meta_value( $object_id, $meta_key, $name, $values ) {
$meta_type = $this->get_meta_type();
if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_update',
/* translators: %s: Custom field key. */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
$current_values = get_metadata( $meta_type, $object_id, $meta_key, false );
$subtype = get_object_subtype( $meta_type, $object_id );
if ( ! is_array( $current_values ) ) {
$current_values = array();
}
$to_remove = $current_values;
$to_add = $values;
foreach ( $to_add as $add_key => $value ) {
$remove_keys = array_keys(
array_filter(
$current_values,
function ( $stored_value ) use ( $meta_key, $subtype, $value ) {
return $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $value );
}
)
);
if ( empty( $remove_keys ) ) {
continue;
}
if ( count( $remove_keys ) > 1 ) {
// To remove, we need to remove first, then add, so don't touch.
continue;
}
$remove_key = $remove_keys[0];
unset( $to_remove[ $remove_key ] );
unset( $to_add[ $add_key ] );
}
/*
* `delete_metadata` removes _all_ instances of the value, so only call once. Otherwise,
* `delete_metadata` will return false for subsequent calls of the same value.
* Use serialization to produce a predictable string that can be used by array_unique.
*/
$to_remove = array_map( 'maybe_unserialize', array_unique( array_map( 'maybe_serialize', $to_remove ) ) );
foreach ( $to_remove as $value ) {
if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
/* translators: %s: Custom field key. */
sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
}
foreach ( $to_add as $value ) {
if ( ! add_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
/* translators: %s: Custom field key. */
sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
}
return true;
}
/**
* Updates a meta value for an object.
*
* @since 4.7.0
*
* @param int $object_id Object ID to update.
* @param string $meta_key Key for the custom field.
* @param string $name Name for the field that is exposed in the REST API.
* @param mixed $value Updated value.
* @return true|WP_Error True if the meta field was updated, WP_Error otherwise.
*/
protected function update_meta_value( $object_id, $meta_key, $name, $value ) {
$meta_type = $this->get_meta_type();
// Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
$old_value = get_metadata( $meta_type, $object_id, $meta_key );
$subtype = get_object_subtype( $meta_type, $object_id );
if ( is_array( $old_value ) && 1 === count( $old_value )
&& $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $old_value[0], $value )
) {
return true;
}
if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
return new WP_Error(
'rest_cannot_update',
/* translators: %s: Custom field key. */
sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
array(
'key' => $name,
'status' => rest_authorization_required_code(),
)
);
}
if ( ! update_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
return new WP_Error(
'rest_meta_database_error',
/* translators: %s: Custom field key. */
sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
array(
'key' => $name,
'status' => WP_Http::INTERNAL_SERVER_ERROR,
)
);
}
return true;
}
/**
* Checks if the user provided value is equivalent to a stored value for the given meta key.
*
* @since 5.5.0
*
* @param string $meta_key The meta key being checked.
* @param string $subtype The object subtype.
* @param mixed $stored_value The currently stored value retrieved from get_metadata().
* @param mixed $user_value The value provided by the user.
* @return bool
*/
protected function is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $user_value ) {
$args = $this->get_registered_fields()[ $meta_key ];
$sanitized = sanitize_meta( $meta_key, $user_value, $this->get_meta_type(), $subtype );
if ( in_array( $args['type'], array( 'string', 'number', 'integer', 'boolean' ), true ) ) {
// The return value of get_metadata will always be a string for scalar types.
$sanitized = (string) $sanitized;
}
return $sanitized === $stored_value;
}
/**
* Retrieves all the registered meta fields.
*
* @since 4.7.0
*
* @return array Registered fields.
*/
protected function get_registered_fields() {
$registered = array();
$meta_type = $this->get_meta_type();
$meta_subtype = $this->get_meta_subtype();
$meta_keys = get_registered_meta_keys( $meta_type );
if ( ! empty( $meta_subtype ) ) {
$meta_keys = array_merge( $meta_keys, get_registered_meta_keys( $meta_type, $meta_subtype ) );
}
foreach ( $meta_keys as $name => $args ) {
if ( empty( $args['show_in_rest'] ) ) {
continue;
}
$rest_args = array();
if ( is_array( $args['show_in_rest'] ) ) {
$rest_args = $args['show_in_rest'];
}
$default_args = array(
'name' => $name,
'single' => $args['single'],
'type' => ! empty( $args['type'] ) ? $args['type'] : null,
'schema' => array(),
'prepare_callback' => array( $this, 'prepare_value' ),
);
$default_schema = array(
'type' => $default_args['type'],
'description' => empty( $args['description'] ) ? '' : $args['description'],
'default' => isset( $args['default'] ) ? $args['default'] : null,
);
$rest_args = array_merge( $default_args, $rest_args );
$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );
$type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null;
$type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type;
if ( null === $rest_args['schema']['default'] ) {
$rest_args['schema']['default'] = static::get_empty_value_for_type( $type );
}
$rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] );
if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ), true ) ) {
continue;
}
if ( empty( $rest_args['single'] ) ) {
$rest_args['schema'] = array(
'type' => 'array',
'items' => $rest_args['schema'],
);
}
$registered[ $name ] = $rest_args;
}
return $registered;
}
/**
* Retrieves the object's meta schema, conforming to JSON Schema.
*
* @since 4.7.0
*
* @return array Field schema data.
*/
public function get_field_schema() {
$fields = $this->get_registered_fields();
$schema = array(
'description' => __( 'Meta fields.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(),
'arg_options' => array(
'sanitize_callback' => null,
'validate_callback' => array( $this, 'check_meta_is_array' ),
),
);
foreach ( $fields as $args ) {
$schema['properties'][ $args['name'] ] = $args['schema'];
}
return $schema;
}
/**
* Prepares a meta value for output.
*
* Default preparation for meta fields. Override by passing the
* `prepare_callback` in your `show_in_rest` options.
*
* @since 4.7.0
*
* @param mixed $value Meta value from the database.
* @param WP_REST_Request $request Request object.
* @param array $args REST-specific options for the meta key.
* @return mixed Value prepared for output. If a non-JsonSerializable object, null.
*/
public static function prepare_value( $value, $request, $args ) {
if ( $args['single'] ) {
$schema = $args['schema'];
} else {
$schema = $args['schema']['items'];
}
if ( '' === $value && in_array( $schema['type'], array( 'boolean', 'integer', 'number' ), true ) ) {
$value = static::get_empty_value_for_type( $schema['type'] );
}
if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
return null;
}
return rest_sanitize_value_from_schema( $value, $schema );
}
/**
* Check the 'meta' value of a request is an associative array.
*
* @since 4.7.0
*
* @param mixed $value The meta value submitted in the request.
* @param WP_REST_Request $request Full details about the request.
* @param string $param The parameter name.
* @return array|false The meta array, if valid, false otherwise.
*/
public function check_meta_is_array( $value, $request, $param ) {
if ( ! is_array( $value ) ) {
return false;
}
return $value;
}
/**
* Recursively add additionalProperties = false to all objects in a schema if no additionalProperties setting
* is specified.
*
* This is needed to restrict properties of objects in meta values to only
* registered items, as the REST API will allow additional properties by
* default.
*
* @since 5.3.0
* @deprecated 5.6.0 Use rest_default_additional_properties_to_false() instead.
*
* @param array $schema The schema array.
* @return array
*/
protected function default_additional_properties_to_false( $schema ) {
_deprecated_function( __METHOD__, '5.6.0', 'rest_default_additional_properties_to_false()' );
return rest_default_additional_properties_to_false( $schema );
}
/**
* Gets the empty value for a schema type.
*
* @since 5.3.0
*
* @param string $type The schema type.
* @return mixed
*/
protected static function get_empty_value_for_type( $type ) {
switch ( $type ) {
case 'string':
return '';
case 'boolean':
return false;
case 'integer':
return 0;
case 'number':
return 0.0;
case 'array':
case 'object':
return array();
default:
return null;
}
}
}
fields/class-wp-rest-post-meta-fields.php 0000644 00000002334 15153067731 0014414 0 ustar 00 <?php
/**
* REST API: WP_REST_Post_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for posts via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Post type to register fields for.
*
* @since 4.7.0
* @var string
*/
protected $post_type;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $post_type Post type to register fields for.
*/
public function __construct( $post_type ) {
$this->post_type = $post_type;
}
/**
* Retrieves the post meta type.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'post';
}
/**
* Retrieves the post meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return $this->post_type;
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @see register_rest_field()
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return $this->post_type;
}
}
fields/class-wp-rest-term-meta-fields.php 0000644 00000002331 15153067731 0014373 0 ustar 00 <?php
/**
* REST API: WP_REST_Term_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for terms via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Taxonomy to register fields for.
*
* @since 4.7.0
* @var string
*/
protected $taxonomy;
/**
* Constructor.
*
* @since 4.7.0
*
* @param string $taxonomy Taxonomy to register fields for.
*/
public function __construct( $taxonomy ) {
$this->taxonomy = $taxonomy;
}
/**
* Retrieves the term meta type.
*
* @since 4.7.0
*
* @return string The meta type.
*/
protected function get_meta_type() {
return 'term';
}
/**
* Retrieves the term meta subtype.
*
* @since 4.9.8
*
* @return string Subtype for the meta type, or empty string if no specific subtype.
*/
protected function get_meta_subtype() {
return $this->taxonomy;
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @return string The REST field type.
*/
public function get_rest_field_type() {
return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
}
}
fields/class-wp-rest-user-meta-fields.php 0000644 00000001530 15153067731 0014402 0 ustar 00 <?php
/**
* REST API: WP_REST_User_Meta_Fields class
*
* @package WordPress
* @subpackage REST_API
* @since 4.7.0
*/
/**
* Core class used to manage meta values for users via the REST API.
*
* @since 4.7.0
*
* @see WP_REST_Meta_Fields
*/
class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {
/**
* Retrieves the user meta type.
*
* @since 4.7.0
*
* @return string The user meta type.
*/
protected function get_meta_type() {
return 'user';
}
/**
* Retrieves the user meta subtype.
*
* @since 4.9.8
*
* @return string 'user' There are no subtypes.
*/
protected function get_meta_subtype() {
return 'user';
}
/**
* Retrieves the type for register_rest_field().
*
* @since 4.7.0
*
* @return string The user REST field type.
*/
public function get_rest_field_type() {
return 'user';
}
}
search/class-wp-rest-post-format-search-handler.php 0000644 00000007544 15153067731 0016377 0 ustar 00 <?php
/**
* REST API: WP_REST_Post_Format_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.6.0
*/
/**
* Core class representing a search handler for post formats in the REST API.
*
* @since 5.6.0
*
* @see WP_REST_Search_Handler
*/
class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler {
/**
* Constructor.
*
* @since 5.6.0
*/
public function __construct() {
$this->type = 'post-format';
}
/**
* Searches the post formats for a given search request.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full REST request.
* @return array {
* Associative array containing found IDs and total count for the matching search results.
*
* @type string[] $ids Array containing slugs for the matching post formats.
* @type int $total Total count for the matching search results.
* }
*/
public function search_items( WP_REST_Request $request ) {
$format_strings = get_post_format_strings();
$format_slugs = array_keys( $format_strings );
$query_args = array();
if ( ! empty( $request['search'] ) ) {
$query_args['search'] = $request['search'];
}
/**
* Filters the query arguments for a REST API post format search request.
*
* Enables adding extra arguments or setting defaults for a post format search request.
*
* @since 5.6.0
*
* @param array $query_args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$query_args = apply_filters( 'rest_post_format_search_query', $query_args, $request );
$found_ids = array();
foreach ( $format_slugs as $index => $format_slug ) {
if ( ! empty( $query_args['search'] ) ) {
$format_string = get_post_format_string( $format_slug );
$format_slug_match = stripos( $format_slug, $query_args['search'] ) !== false;
$format_string_match = stripos( $format_string, $query_args['search'] ) !== false;
if ( ! $format_slug_match && ! $format_string_match ) {
continue;
}
}
$format_link = get_post_format_link( $format_slug );
if ( $format_link ) {
$found_ids[] = $format_slug;
}
}
$page = (int) $request['page'];
$per_page = (int) $request['per_page'];
return array(
self::RESULT_IDS => array_slice( $found_ids, ( $page - 1 ) * $per_page, $per_page ),
self::RESULT_TOTAL => count( $found_ids ),
);
}
/**
* Prepares the search result for a given post format.
*
* @since 5.6.0
*
* @param string $id Item ID, the post format slug.
* @param array $fields Fields to include for the item.
* @return array {
* Associative array containing fields for the post format based on the `$fields` parameter.
*
* @type string $id Optional. Post format slug.
* @type string $title Optional. Post format name.
* @type string $url Optional. Post format permalink URL.
* @type string $type Optional. String 'post-format'.
*}
*/
public function prepare_item( $id, array $fields ) {
$data = array();
if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_ID ] = $id;
}
if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TITLE ] = get_post_format_string( $id );
}
if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_URL ] = get_post_format_link( $id );
}
if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
}
return $data;
}
/**
* Prepares links for the search result.
*
* @since 5.6.0
*
* @param string $id Item ID, the post format slug.
* @return array Links for the given item.
*/
public function prepare_item_links( $id ) {
return array();
}
}
search/class-wp-rest-post-search-handler.php 0000644 00000013370 15153067731 0015103 0 ustar 00 <?php
/**
* REST API: WP_REST_Post_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core class representing a search handler for posts in the REST API.
*
* @since 5.0.0
*
* @see WP_REST_Search_Handler
*/
class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler {
/**
* Constructor.
*
* @since 5.0.0
*/
public function __construct() {
$this->type = 'post';
// Support all public post types except attachments.
$this->subtypes = array_diff(
array_values(
get_post_types(
array(
'public' => true,
'show_in_rest' => true,
),
'names'
)
),
array( 'attachment' )
);
}
/**
* Searches posts for a given search request.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full REST request.
* @return array {
* Associative array containing found IDs and total count for the matching search results.
*
* @type int[] $ids Array containing the matching post IDs.
* @type int $total Total count for the matching search results.
* }
*/
public function search_items( WP_REST_Request $request ) {
// Get the post types to search for the current request.
$post_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $post_types, true ) ) {
$post_types = $this->subtypes;
}
$query_args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'paged' => (int) $request['page'],
'posts_per_page' => (int) $request['per_page'],
'ignore_sticky_posts' => true,
);
if ( ! empty( $request['search'] ) ) {
$query_args['s'] = $request['search'];
}
if ( ! empty( $request['exclude'] ) ) {
$query_args['post__not_in'] = $request['exclude'];
}
if ( ! empty( $request['include'] ) ) {
$query_args['post__in'] = $request['include'];
}
/**
* Filters the query arguments for a REST API post search request.
*
* Enables adding extra arguments or setting defaults for a post search request.
*
* @since 5.1.0
*
* @param array $query_args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$query_args = apply_filters( 'rest_post_search_query', $query_args, $request );
$query = new WP_Query();
$posts = $query->query( $query_args );
// Querying the whole post object will warm the object cache, avoiding an extra query per result.
$found_ids = wp_list_pluck( $posts, 'ID' );
$total = $query->found_posts;
return array(
self::RESULT_IDS => $found_ids,
self::RESULT_TOTAL => $total,
);
}
/**
* Prepares the search result for a given post ID.
*
* @since 5.0.0
*
* @param int $id Post ID.
* @param array $fields Fields to include for the post.
* @return array {
* Associative array containing fields for the post based on the `$fields` parameter.
*
* @type int $id Optional. Post ID.
* @type string $title Optional. Post title.
* @type string $url Optional. Post permalink URL.
* @type string $type Optional. Post type.
* }
*/
public function prepare_item( $id, array $fields ) {
$post = get_post( $id );
$data = array();
if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $post->ID;
}
if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
if ( post_type_supports( $post->post_type, 'title' ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
$data[ WP_REST_Search_Controller::PROP_TITLE ] = get_the_title( $post->ID );
remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
} else {
$data[ WP_REST_Search_Controller::PROP_TITLE ] = '';
}
}
if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_URL ] = get_permalink( $post->ID );
}
if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
}
if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $post->post_type;
}
return $data;
}
/**
* Prepares links for the search result of a given ID.
*
* @since 5.0.0
*
* @param int $id Item ID.
* @return array Links for the given item.
*/
public function prepare_item_links( $id ) {
$post = get_post( $id );
$links = array();
$item_route = rest_get_route_for_post( $post );
if ( ! empty( $item_route ) ) {
$links['self'] = array(
'href' => rest_url( $item_route ),
'embeddable' => true,
);
}
$links['about'] = array(
'href' => rest_url( 'wp/v2/types/' . $post->post_type ),
);
return $links;
}
/**
* Overwrites the default protected title format.
*
* By default, WordPress will show password protected posts with a title of
* "Protected: %s". As the REST API communicates the protected status of a post
* in a machine readable format, we remove the "Protected: " prefix.
*
* @since 5.0.0
*
* @return string Protected title format.
*/
public function protected_title_format() {
return '%s';
}
/**
* Attempts to detect the route to access a single item.
*
* @since 5.0.0
* @deprecated 5.5.0 Use rest_get_route_for_post()
* @see rest_get_route_for_post()
*
* @param WP_Post $post Post object.
* @return string REST route relative to the REST base URI, or empty string if unknown.
*/
protected function detect_rest_item_route( $post ) {
_deprecated_function( __METHOD__, '5.5.0', 'rest_get_route_for_post()' );
return rest_get_route_for_post( $post );
}
}
search/class-wp-rest-search-handler.php 0000644 00000004367 15153067731 0014126 0 ustar 00 <?php
/**
* REST API: WP_REST_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/
/**
* Core base class representing a search handler for an object type in the REST API.
*
* @since 5.0.0
*/
#[AllowDynamicProperties]
abstract class WP_REST_Search_Handler {
/**
* Field containing the IDs in the search result.
*/
const RESULT_IDS = 'ids';
/**
* Field containing the total count in the search result.
*/
const RESULT_TOTAL = 'total';
/**
* Object type managed by this search handler.
*
* @since 5.0.0
* @var string
*/
protected $type = '';
/**
* Object subtypes managed by this search handler.
*
* @since 5.0.0
* @var string[]
*/
protected $subtypes = array();
/**
* Gets the object type managed by this search handler.
*
* @since 5.0.0
*
* @return string Object type identifier.
*/
public function get_type() {
return $this->type;
}
/**
* Gets the object subtypes managed by this search handler.
*
* @since 5.0.0
*
* @return string[] Array of object subtype identifiers.
*/
public function get_subtypes() {
return $this->subtypes;
}
/**
* Searches the object type content for a given search request.
*
* @since 5.0.0
*
* @param WP_REST_Request $request Full REST request.
* @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing
* an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the
* total count for the matching search results.
*/
abstract public function search_items( WP_REST_Request $request );
/**
* Prepares the search result for a given ID.
*
* @since 5.0.0
* @since 5.6.0 The `$id` parameter can accept a string.
*
* @param int|string $id Item ID.
* @param array $fields Fields to include for the item.
* @return array Associative array containing all fields for the item.
*/
abstract public function prepare_item( $id, array $fields );
/**
* Prepares links for the search result of a given ID.
*
* @since 5.0.0
* @since 5.6.0 The `$id` parameter can accept a string.
*
* @param int|string $id Item ID.
* @return array Links for the given item.
*/
abstract public function prepare_item_links( $id );
}
search/class-wp-rest-term-search-handler.php 0000644 00000011032 15153067731 0015056 0 ustar 00 <?php
/**
* REST API: WP_REST_Term_Search_Handler class
*
* @package WordPress
* @subpackage REST_API
* @since 5.6.0
*/
/**
* Core class representing a search handler for terms in the REST API.
*
* @since 5.6.0
*
* @see WP_REST_Search_Handler
*/
class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler {
/**
* Constructor.
*
* @since 5.6.0
*/
public function __construct() {
$this->type = 'term';
$this->subtypes = array_values(
get_taxonomies(
array(
'public' => true,
'show_in_rest' => true,
),
'names'
)
);
}
/**
* Searches terms for a given search request.
*
* @since 5.6.0
*
* @param WP_REST_Request $request Full REST request.
* @return array {
* Associative array containing found IDs and total count for the matching search results.
*
* @type int[] $ids Found term IDs.
* @type string|int|WP_Error $total Numeric string containing the number of terms in that
* taxonomy, 0 if there are no results, or WP_Error if
* the requested taxonomy does not exist.
* }
*/
public function search_items( WP_REST_Request $request ) {
$taxonomies = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $taxonomies, true ) ) {
$taxonomies = $this->subtypes;
}
$page = (int) $request['page'];
$per_page = (int) $request['per_page'];
$query_args = array(
'taxonomy' => $taxonomies,
'hide_empty' => false,
'offset' => ( $page - 1 ) * $per_page,
'number' => $per_page,
);
if ( ! empty( $request['search'] ) ) {
$query_args['search'] = $request['search'];
}
if ( ! empty( $request['exclude'] ) ) {
$query_args['exclude'] = $request['exclude'];
}
if ( ! empty( $request['include'] ) ) {
$query_args['include'] = $request['include'];
}
/**
* Filters the query arguments for a REST API term search request.
*
* Enables adding extra arguments or setting defaults for a term search request.
*
* @since 5.6.0
*
* @param array $query_args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$query_args = apply_filters( 'rest_term_search_query', $query_args, $request );
$query = new WP_Term_Query();
$found_terms = $query->query( $query_args );
$found_ids = wp_list_pluck( $found_terms, 'term_id' );
unset( $query_args['offset'], $query_args['number'] );
$total = wp_count_terms( $query_args );
// wp_count_terms() can return a falsey value when the term has no children.
if ( ! $total ) {
$total = 0;
}
return array(
self::RESULT_IDS => $found_ids,
self::RESULT_TOTAL => $total,
);
}
/**
* Prepares the search result for a given term ID.
*
* @since 5.6.0
*
* @param int $id Term ID.
* @param array $fields Fields to include for the term.
* @return array {
* Associative array containing fields for the term based on the `$fields` parameter.
*
* @type int $id Optional. Term ID.
* @type string $title Optional. Term name.
* @type string $url Optional. Term permalink URL.
* @type string $type Optional. Term taxonomy name.
* }
*/
public function prepare_item( $id, array $fields ) {
$term = get_term( $id );
$data = array();
if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $id;
}
if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TITLE ] = $term->name;
}
if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_URL ] = get_term_link( $id );
}
if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
$data[ WP_REST_Search_Controller::PROP_TYPE ] = $term->taxonomy;
}
return $data;
}
/**
* Prepares links for the search result of a given ID.
*
* @since 5.6.0
*
* @param int $id Item ID.
* @return array[] Array of link arrays for the given item.
*/
public function prepare_item_links( $id ) {
$term = get_term( $id );
$links = array();
$item_route = rest_get_route_for_term( $term );
if ( $item_route ) {
$links['self'] = array(
'href' => rest_url( $item_route ),
'embeddable' => true,
);
}
$links['about'] = array(
'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $term->taxonomy ) ),
);
return $links;
}
}
class-snippets-rest-controller.php 0000644 00000036051 15153753666 0013407 0 ustar 00 <?php
namespace Code_Snippets\REST_API;
use Code_Snippets\Export;
use Code_Snippets\Snippet;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use function Code_Snippets\activate_snippet;
use function Code_Snippets\code_snippets;
use function Code_Snippets\deactivate_snippet;
use function Code_Snippets\delete_snippet;
use function Code_Snippets\get_snippet;
use function Code_Snippets\get_snippets;
use function Code_Snippets\save_snippet;
use const Code_Snippets\REST_API_NAMESPACE;
/**
* Allows fetching snippet data through the WordPress REST API.
*
* @since 3.4.0
* @package Code_Snippets
*/
final class Snippets_REST_Controller extends WP_REST_Controller {
/**
* Current API version.
*/
const VERSION = 1;
/**
* The base of this controller's route.
*/
const BASE_ROUTE = 'snippets';
/**
* The namespace of this controller's route.
*
* @var string
*/
protected $namespace = REST_API_NAMESPACE . self::VERSION;
/**
* The base of this controller's route.
*
* @var string
*/
protected $rest_base = self::BASE_ROUTE;
/**
* Retrieve this controller's REST API base path, including namespace.
*
* @return string
*/
public static function get_base_route(): string {
return REST_API_NAMESPACE . self::VERSION . '/' . self::BASE_ROUTE;
}
/**
* Retrieve the full base route including the REST API prefix.
*
* @return string
*/
public static function get_prefixed_base_route(): string {
return '/' . rtrim( rest_get_url_prefix(), '/\\' ) . '/' . self::get_base_route();
}
/**
* Register REST routes.
*/
public function register_routes() {
$route = '/' . $this->rest_base;
$id_route = $route . '/(?P<id>[\d]+)';
$network_args = array_intersect_key(
$this->get_endpoint_args_for_item_schema(),
[ 'network' ]
);
register_rest_route(
$this->namespace,
$route,
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'permission_callback' => [ $this, 'get_items_permissions_check' ],
'args' => $network_args,
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'permission_callback' => [ $this, 'create_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( true ),
],
'schema' => [ $this, 'get_item_schema' ],
]
);
register_rest_route(
$this->namespace,
$id_route,
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'permission_callback' => [ $this, 'get_item_permissions_check' ],
'args' => $network_args,
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'update_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( false ),
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [ $this, 'delete_item' ],
'permission_callback' => [ $this, 'delete_item_permissions_check' ],
'args' => $network_args,
],
'schema' => [ $this, 'get_item_schema' ],
]
);
register_rest_route(
$this->namespace,
$route . '/schema',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_public_item_schema' ],
'permission_callback' => '__return_true',
]
);
register_rest_route(
$this->namespace,
$id_route . '/activate',
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'activate_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
);
register_rest_route(
$this->namespace,
$id_route . '/deactivate',
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'deactivate_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
);
register_rest_route(
$this->namespace,
$id_route . '/export',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'export_item' ],
'permission_callback' => [ $this, 'get_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
);
register_rest_route(
$this->namespace,
$id_route . '/export-code',
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'export_item_code' ],
'permission_callback' => [ $this, 'get_item_permissions_check' ],
'schema' => [ $this, 'get_item_schema' ],
'args' => $network_args,
]
);
}
/**
* Retrieves a collection of snippets.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response Response object on success.
*/
public function get_items( $request ): WP_REST_Response {
$snippets = get_snippets();
$snippets_data = [];
foreach ( $snippets as $snippet ) {
$snippet_data = $this->prepare_item_for_response( $snippet, $request );
$snippets_data[] = $this->prepare_response_for_collection( $snippet_data );
}
return rest_ensure_response( $snippets_data );
}
/**
* Retrieves one item from the collection.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error Response object on success.
*/
public function get_item( $request ) {
$snippet_id = $request->get_param( 'id' );
$item = get_snippet( $snippet_id, $request->get_param( 'network' ) );
if ( ! $item->id && 0 !== $snippet_id && '0' !== $snippet_id ) {
return new WP_Error(
'rest_cannot_get',
__( 'The snippet could not be found.', 'code-snippets' ),
[ 'status' => 500 ]
);
}
$data = $this->prepare_item_for_response( $item, $request );
return rest_ensure_response( $data );
}
/**
* Create one item from the collection
*
* @param WP_REST_Request|array $request Full data about the request.
*
* @return WP_REST_Response|WP_Error
*/
public function create_item( $request ) {
$snippet = $this->prepare_item_for_database( $request );
$result = $snippet ? save_snippet( $snippet ) : null;
return $result ?
$this->prepare_item_for_response( $result, $request ) :
new WP_Error(
'rest_cannot_create',
__( 'The snippet could not be created.', 'code-snippets' ),
[ 'status' => 500 ]
);
}
/**
* Update one item from the collection
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$snippet_id = absint( $request->get_param( 'id' ) );
$snippet = $snippet_id ? get_snippet( $snippet_id, $request->get_param( 'network' ) ) : null;
if ( ! $snippet_id || ! $snippet || ! $snippet->id ) {
return new WP_Error(
'rest_cannot_update',
__( 'Cannot update a snippet without a valid ID.', 'code-snippets' ),
[ 'status' => 400 ]
);
}
$item = $this->prepare_item_for_database( $request, $snippet );
$result = save_snippet( $item );
if ( $result ) {
$request->set_param( 'id', $result->id );
return $this->get_item( $request );
}
return new WP_Error(
'rest_cannot_update',
__( 'The snippet could not be updated.', 'code-snippets' ),
[ 'status' => 500 ]
);
}
/**
* Delete one item from the collection
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
$item = $this->prepare_item_for_database( $request );
$result = delete_snippet( $item->id, $item->network );
return $result ?
new WP_REST_Response( null, 204 ) :
new WP_Error(
'rest_cannot_delete',
__( 'The snippet could not be deleted.', 'code-snippets' ),
[ 'status' => 500 ]
);
}
/**
* Activate one item in the collection.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function activate_item( WP_REST_Request $request ) {
$item = $this->prepare_item_for_database( $request );
$result = activate_snippet( $item->id, $item->network );
return $result instanceof Snippet ?
rest_ensure_response( $result ) :
new WP_Error(
'rest_cannot_activate',
$result,
[ 'status' => 500 ]
);
}
/**
* Deactivate one item in the collection.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function deactivate_item( WP_REST_Request $request ) {
$item = $this->prepare_item_for_database( $request );
$result = deactivate_snippet( $item->id, $item->network );
return $result instanceof Snippet ?
rest_ensure_response( $result ) :
new WP_Error(
'rest_cannot_activate',
__( 'The snippet could not be deactivated.', 'code-snippets' ),
[ 'status' => 500 ]
);
}
/**
* Prepare an instance of the Export class from a request.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return Export
*/
protected function build_export( WP_REST_Request $request ): Export {
$item = $this->prepare_item_for_database( $request );
return new Export( [ $item->id ], $item->network );
}
/**
* Retrieve one item in the collection in JSON export format.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function export_item( WP_REST_Request $request ) {
$export = $this->build_export( $request );
$result = $export->create_export_object();
return rest_ensure_response( $result );
}
/**
* Retrieve one item in the collection in the code export format.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return WP_Error|WP_REST_Response
*/
public function export_item_code( WP_REST_Request $request ) {
$export = $this->build_export( $request );
$result = $export->export_snippets_code();
return rest_ensure_response( $result );
}
/**
* Prepares one item for create or update operation.
*
* @param WP_REST_Request $request Request object.
* @param Snippet|null $item Existing item to augment.
*
* @return Snippet The prepared item.
*/
protected function prepare_item_for_database( $request, ?Snippet $item = null ): ?Snippet {
if ( ! $item instanceof Snippet ) {
$item = new Snippet();
}
foreach ( $item->get_allowed_fields() as $field ) {
if ( isset( $request[ $field ] ) ) {
$item->set_field( $field, $request[ $field ] );
}
}
return $item;
}
/**
* Prepare the item for the REST response.
*
* @param Snippet $item Snippet object.
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$schema = $this->get_item_schema();
$response = [];
foreach ( array_keys( $schema['properties'] ) as $property ) {
$response[ $property ] = $item->$property;
}
return rest_ensure_response( $response );
}
/**
* Check if a given request has access to get items.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return boolean
*/
public function get_items_permissions_check( $request ): bool {
return code_snippets()->current_user_can();
}
/**
* Check if a given request has access to get a specific item.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return boolean
*/
public function get_item_permissions_check( $request ): bool {
return $this->get_items_permissions_check( $request );
}
/**
* Check if a given request has access to create items.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return boolean
*/
public function create_item_permissions_check( $request ): bool {
return code_snippets()->current_user_can();
}
/**
* Check if a given request has access to update a specific item.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return boolean
*/
public function update_item_permissions_check( $request ): bool {
return $this->create_item_permissions_check( $request );
}
/**
* Check if a given request has access to delete a specific item.
*
* @param WP_REST_Request $request Full data about the request.
*
* @return boolean
*/
public function delete_item_permissions_check( $request ): bool {
return $this->create_item_permissions_check( $request );
}
/**
* Get our sample schema for a post.
*
* @return array<string, mixed> The sample schema for a post
*/
public function get_item_schema(): array {
if ( $this->schema ) {
return $this->schema;
}
$this->schema = [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'snippet',
'type' => 'object',
'properties' => [
'id' => [
'description' => esc_html__( 'Unique identifier for the snippet.', 'code-snippets' ),
'type' => 'integer',
'readonly' => true,
],
'name' => [
'description' => esc_html__( 'Descriptive title for the snippet.', 'code-snippets' ),
'type' => 'string',
],
'desc' => [
'description' => esc_html__( 'Descriptive text associated with snippet.', 'code-snippets' ),
'type' => 'string',
],
'code' => [
'description' => esc_html__( 'Executable snippet code.', 'code-snippets' ),
'type' => 'string',
],
'tags' => [
'description' => esc_html__( 'List of tag categories the snippet belongs to.', 'code-snippets' ),
'type' => 'array',
],
'scope' => [
'description' => esc_html__( 'Context in which the snippet is executable.', 'code-snippets' ),
'type' => 'string',
],
'active' => [
'description' => esc_html__( 'Snippet activation status.', 'code-snippets' ),
'type' => 'boolean',
],
'priority' => [
'description' => esc_html__( 'Relative priority in which the snippet is executed.', 'code-snippets' ),
'type' => 'integer',
],
'network' => [
'description' => esc_html__( 'Whether the snippet is network-wide instead of site-wide.', 'code-snippets' ),
'type' => 'boolean',
'default' => null,
],
'shared_network' => [
'description' => esc_html__( 'If a network snippet, whether can be activated on discrete sites instead of network-wide.', 'code-snippets' ),
'type' => 'boolean',
],
'modified' => [
'description' => esc_html__( 'Date and time when the snippet was last modified, in ISO format.', 'code-snippets' ),
'type' => 'string',
],
],
];
return $this->schema;
}
}
class-yith-wcwl-rest-server.php 0000644 00000003142 15154021271 0012564 0 ustar 00 <?php
/**
* Initialize this version of the REST API.
*
* @package YITH\Wishlist\RestApi
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'YITH_WCWL_Rest_Server' ) ) {
/**
* Class responsible for loading the REST API and all REST API namespaces.
*/
class YITH_WCWL_Rest_Server {
use YITH_WCWL_Singleton_Trait;
/**
* REST API namespaces and endpoints.
*
* @var array
*/
protected $controllers = array();
/**
* Construct
*/
protected function __construct() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
}
/**
* Instance the controller classes that will register the Rest Routes
*/
public function register_rest_routes() {
foreach ( $this->get_rest_namespaces() as $namespace => $controllers ) {
foreach ( $controllers as $controller_name => $controller_class ) {
if ( class_exists( $controller_class ) ) {
$this->controllers[ $namespace ][ $controller_name ] = new $controller_class();
$this->controllers[ $namespace ][ $controller_name ]->register_routes();
}
}
}
}
/**
* Get the namespace that needs to be registered
*
* @return array[]
*/
protected function get_rest_namespaces() {
return array(
'yith-wishlist' => $this->get_v1_controllers(),
);
}
/**
* List of controllers in the v1 namespace.
*
* @return array
*/
protected function get_v1_controllers() {
return array(
'products' => 'YITH_WCWL_Rest_V1_Products_Controller',
'lists' => 'YITH_WCWL_Rest_V1_Lists_Controller',
'items' => 'YITH_WCWL_Rest_V1_Items_Controller',
);
}
}
}
controllers/v1/class-yith-wcwl-rest-v1-controller.php 0000644 00000005566 15154021271 0016675 0 ustar 00 <?php
/**
* Rest controller abstract class
*/
defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
if ( ! class_exists( 'YITH_WCWL_Rest_V1_Controller' ) ) {
abstract class YITH_WCWL_Rest_V1_Controller extends WP_REST_Controller {
/**
* Get item permission
*
* TODO: implement this method
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function get_item_permissions_check( $request ) {
return true;
}
/**
* Get item permission
*
* TODO: implement this method
*
* @param $request
* @return true|WP_Error
*/
public function get_items_permissions_check( $request ) {
return true;
}
/**
* @param YITH_WCWL_Wishlist[] $wishlists The wishlist objects.
*
* @return array
*/
protected function prepare_wishlists_for_rest( $wishlists ) {
$wishlists_for_rest = array();
foreach ( $wishlists as $wishlist ) {
$wishlists_for_rest[ $wishlist->get_id() ] = $this->prepare_wishlist_for_rest( $wishlist );
}
return $wishlists_for_rest;
}
/**
* Prepare wishlist object for Rest response
*
* @param int|YITH_WCWL_Wishlist $wishlist The wishlist object or its id.
*
* @return array|false
*/
protected function prepare_wishlist_for_rest( $wishlist ) {
$wishlist = $wishlist instanceof YITH_WCWL_Wishlist ? $wishlist : YITH_WCWL_Wishlist_Factory::get_wishlist( $wishlist );
if ( ! $wishlist ) {
return false;
}
return array_merge(
$wishlist->get_data(),
array(
'name' => $wishlist->get_formatted_name(),
'token' => $wishlist->get_token(),
'session_id' => $wishlist->get_session_id(),
)
);
}
/**
* @param YITH_WCWL_Wishlist[] $wishlists The wishlist objects.
*
* @return array
*/
protected function prepare_products_for_rest( $products ) {
$products_for_rest = array();
foreach ( $products as $product ) {
$products_for_rest[ $product instanceof WC_Product ? $product->get_id() : absint( $product ) ] = $this->prepare_product_for_rest( $product );
}
return $products_for_rest;
}
/**
* Prepare wishlist object for Rest response
*
* @param int|WC_Product $product The product object or its id.
*
* @return array|false
*/
protected function prepare_product_for_rest( $product, $additional_data = array() ) {
$product = $product instanceof WC_Product ? $product : wc_get_product( $product );
if ( ! $product ) {
return false;
}
$product_id = $product->get_id();
$product_lists = yith_wcwl_wishlists()->get_wishlists_for_product( $product_id );
return array_merge(
array(
'name' => $product->get_name(),
'isAdded' => ! ! $product_lists,
'count' => yith_wcwl_wishlists()->count_add_to_wishlist( $product_id ),
'wishlists' => $product_lists,
),
is_array( $additional_data ) ? $additional_data : array()
);
}
}
}
controllers/v1/class-yith-wcwl-rest-v1-items-controller.php 0000644 00000022036 15154021271 0020003 0 ustar 00 <?php
/**
* REST API Wishlist Items controller class.
*
* @package YITH\Wishlist\RestApi
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'YITH_WCWL_Rest_V1_Items_Controller' ) ) {
/**
* REST API Wishlist Items controller class.
*
* @package YITH\Wishlist\RestApi
*/
class YITH_WCWL_Rest_V1_Items_Controller extends YITH_WCWL_Rest_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'yith/wishlist/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'items';
/**
* Register the routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'add_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array(
'product_id' => array(
'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
'required' => true,
),
'wishlist_id' => array(
'description' => _x( 'The Wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => array( 'integer', 'string' ),
),
'quantity' => array(
'description' => _x( 'The quantity to add in wishlist', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
'user_id' => array(
'description' => _x( 'The User ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
'dateadded' => array(
'description' => _x( 'The timestamp to use as added date.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
'wishlist_name' => array(
'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'string',
),
'wishlist_visibility' => array(
'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'remove_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'product_id' => array(
'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
'required' => true,
),
'wishlist_id' => array(
'description' => _x( 'The Wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => array( 'integer', 'string' ),
),
'user_id' => array(
'description' => _x( 'The User ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/move',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'move_item' ),
'permission_callback' => '__return_true',
'args' => array(
'product_id' => array(
'description' => _x( 'The product ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
'required' => true,
),
'destination_wishlist' => array(
'description' => _x( 'The destination wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => array( 'integer', 'string' ),
'required' => true,
),
'origin_wishlist' => array(
'description' => _x( 'The origin wishlist ID', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
'wishlist_name' => array(
'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'string',
),
'wishlist_visibility' => array(
'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Add item to a wishlist
*
* @param WP_REST_Request $request The rest request.
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function add_item( $request ) {
$args = $request->get_params();
$response = array( 'success' => true );
$creating_default = false;
if ( ! empty( $args[ 'wishlist_id' ] ) && 'default' === $args[ 'wishlist_id' ] ) {
$creating_default = true;
unset( $args[ 'wishlist_id' ] );
}
try {
$data = YITH_WCWL_Wishlists::get_instance()->add_item( $args );
$product_id = $args[ 'product_id' ];
$response = array(
'product_data' => array_merge(
$this->prepare_product_for_rest( $product_id ),
array(
'added_to' => $data[ 'wishlist_id' ],
)
),
);
if ( $creating_default ) {
$response[ 'wishlist_data' ] = $this->prepare_wishlist_for_rest( $data[ 'wishlist_id' ] );
}
} catch ( \Exception $e ) {
$response = array( 'success' => false, 'message' => $e->getMessage() );
}
return rest_ensure_response( $response );
}
/**
* Remove an item from a wishlist
*
* @param WP_REST_Request $request The rest request.
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function remove_item( $request ) {
$args = $request->get_params();
$response = array( 'success' => true );
try {
$data = YITH_WCWL_Wishlists::get_instance()->remove_item( $args );
$product_id = $args[ 'product_id' ];
$response = array(
'product_data' => array_merge(
array( 'removed_from' => $data[ 'wishlist_id' ] ),
$this->prepare_product_for_rest( $product_id )
),
);
} catch ( \Exception $e ) {
$response = array( 'success' => false, 'message' => $e->getMessage() );
}
return rest_ensure_response( $response );
}
/**
* Remove an item from a wishlist
*
* @param WP_REST_Request $request The rest request.
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function move_item( $request ) {
$args = $request->get_params();
try {
$result = YITH_WCWL_Wishlists::get_instance()->move( $args );
$response = array(
'moved' => $result[ 'moved' ],
'product_data' => $this->prepare_product_for_rest(
$args[ 'product_id' ],
array(
'moved_from' => $args[ 'origin_wishlist' ],
'moved_to' => 'new' === $args[ 'destination_wishlist' ] ? $result[ 'destination_wishlist' ]->get_id() : $args[ 'destination_wishlist' ],
)
),
);
if ( $result[ 'destination_wishlist' ] instanceof YITH_WCWL_Wishlist ) {
$response[ 'destination_wishlist' ] = $this->prepare_wishlist_for_rest( $result[ 'destination_wishlist' ] );
}
} catch ( \Exception $e ) {
$response = array( 'success' => false, 'message' => $e->getMessage() );
}
return rest_ensure_response( $response );
}
/**
* Create item permission checks
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function create_item_permissions_check( $request ) {
$wishlist = $this->get_wishlist_from_request( $request );
return ! $wishlist || $wishlist->current_user_can( 'add_to_wishlist' );
}
/**
* Delete item permission checks
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$wishlist = $this->get_wishlist_from_request( $request );
return ! $wishlist || $wishlist->current_user_can( 'remove_from_wishlist' );
}
/**
* Get wishlist object from request
*
* @param WP_REST_Request $request The request.
*
* @return YITH_WCWL_Wishlist|false
*/
protected function get_wishlist_from_request( $request ) {
$wishlist_id = $request->get_param( 'wishlist_id' );
$wishlist = false;
if ( $wishlist_id ) {
try {
$wishlist = new YITH_WCWL_Wishlist( $wishlist_id );
} catch ( Exception $exception ) {
}
}
return $wishlist;
}
}
}
controllers/v1/class-yith-wcwl-rest-v1-lists-controller.php 0000644 00000006137 15154021271 0020024 0 ustar 00 <?php
/**
* REST API Wishlist Lists controller class.
*
* @package YITH\Wishlist\RestApi
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'YITH_WCWL_Rest_V1_Lists_Controller' ) ) {
/**
* REST API Wishlist Lists controller class.
*
* @package YITH\Wishlist\RestApi
*/
class YITH_WCWL_Rest_V1_Lists_Controller extends YITH_WCWL_Rest_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'yith/wishlist/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'lists';
/**
* Register the routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_lists' ),
'permission_callback' => '__return_true',
'args' => $this->get_endpoint_args_for_item_schema(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_list' ),
'permission_callback' => '__return_true',
'args' => array(
'wishlist_name' => array(
'description' => _x( 'The wishlist name.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'string',
'required' => true,
),
'wishlist_visibility' => array(
'description' => _x( 'The wishlist visibility value.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
'required' => true,
),
'user_id' => array(
'description' => _x( 'The unique identifier for the user.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
),
'session_id' => array(
'description' => _x( 'The unique identifier for the session.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get lists
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function get_lists() {
$response = array(
'lists' => $this->prepare_wishlists_for_rest( YITH_WCWL_Wishlists::get_instance()->get_current_user_wishlists() ),
);
return rest_ensure_response( $response );
}
public function create_list( $request ) {
$args = $request->get_params();
try {
$wishlist = YITH_WCWL_Wishlists::get_instance()->create( $args );
$response = array(
'wishlist_data' => $this->prepare_wishlist_for_rest( $wishlist ),
);
} catch ( YITH_WCWL_Exception $e ) {
$response = array(
'success' => false,
'message' => $e->getMessage(),
);
}
return rest_ensure_response( $response );
}
}
}
controllers/v1/class-yith-wcwl-rest-v1-products-controller.php 0000644 00000005647 15154021271 0020536 0 ustar 00 <?php
/**
* REST API Wishlist Products controller class.
*
* @package YITH\Wishlist\RestApi
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'YITH_WCWL_REST_V1_Products_Controller' ) ) {
/**
* REST API Wishlist Products controller class.
*
* @package YITH\Wishlist\RestApi
*/
class YITH_WCWL_Rest_V1_Products_Controller extends YITH_WCWL_Rest_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'yith/wishlist/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products';
/**
* Register the routes.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_products_data' ),
'permission_callback' => '__return_true', // TODO: implement checks for permissions.
'args' => array(
'product_ids' => array(
'description' => _x( 'The list of product ids for which data is being requested.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'array',
'required' => true,
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<product_id>[\d]+)',
array(
'args' => array(
'product_id' => array(
'description' => _x( 'The unique identifier for the product.', '[REST-API] The schema field description', 'yith-woocommerce-wishlist' ),
'type' => 'integer',
'required' => true,
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_product_data' ),
'permission_callback' => '__return_true',
'args' => $this->get_endpoint_args_for_item_schema(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get wishlist data related to multiple products
*
* @param WP_REST_Request $request The rest request.
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function get_products_data( $request ) {
$args = $request->get_params();
$products_data = array();
if ( ! empty( $args[ 'product_ids' ] ) ) {
$products_data = $this->prepare_products_for_rest( $args[ 'product_ids' ] );
}
return rest_ensure_response( $products_data );
}
/**
* Get wishlist data related to a single products
*
* @param WP_REST_Request $request The rest request.
*
* @return WP_Error|WP_HTTP_Response|WP_REST_Response
*/
public function get_product_data( $request ) {
$args = $request->get_params();
$product_id = $args[ 'product_id' ];
$response = $this->prepare_product_for_rest($product_id);
return rest_ensure_response( $response );
}
}
}
Controllers/Telemetry/class-wc-rest-telemetry-controller.php 0000644 00000011107 15154275217 0020426 0 ustar 00 <?php
/**
* REST API WC Telemetry controller
*
* Handles requests to the /wc-telemetry endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Telemetry controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Telemetry_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-telemetry';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'tracker';
/**
* Register the route for /tracker
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'record_usage_data' ),
'permission_callback' => array( $this, 'telemetry_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to post telemetry data
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function telemetry_permissions_check( $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you post telemetry data.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Record WCTracker Data
*
* @param WP_REST_Request $request Full details about the request.
*/
public function record_usage_data( $request ) {
$new = $this->get_usage_data( $request );
if ( ! $new || ! $new['platform'] ) {
return;
}
$data = get_option( 'woocommerce_mobile_app_usage' );
if ( ! $data ) {
$data = array();
}
$platform = $new['platform'];
if ( isset( $data[ $platform ] ) ) {
$existing_usage = $data[ $platform ];
// Sets the installation date only if it has not been set before.
if ( isset( $new['installation_date'] ) && ! isset( $existing_usage['installation_date'] ) ) {
$data[ $platform ]['installation_date'] = $new['installation_date'];
}
if ( version_compare( $new['version'], $existing_usage['version'], '>=' ) ) {
$data[ $platform ]['version'] = $new['version'];
$data[ $platform ]['last_used'] = $new['last_used'];
}
} else {
// Only sets `first_used` when the platform usage data hasn't been set before.
$new['first_used'] = $new['last_used'];
$data[ $platform ] = $new;
}
update_option( 'woocommerce_mobile_app_usage', $data );
}
/**
* Get usage data from current request
*
* @param WP_REST_Request $request Full details about the request.
* @return Array
*/
public function get_usage_data( $request ) {
$platform = strtolower( $request->get_param( 'platform' ) );
switch ( $platform ) {
case 'ios':
case 'android':
break;
default:
return;
}
$version = $request->get_param( 'version' );
if ( ! $version ) {
return;
}
// The installation date could be null from earlier mobile client versions.
$installation_date = $request->get_param( 'installation_date' );
return array_filter(
array(
'platform' => sanitize_text_field( $platform ),
'version' => sanitize_text_field( $version ),
'last_used' => gmdate( 'c' ),
'installation_date' => isset( $installation_date ) ? get_gmt_from_date( $installation_date, 'c' ) : null,
),
function( $value ) {
return null !== $value;
}
);
}
/**
* Get any query params needed.
*
* @return array
*/
public function get_collection_params() {
return array(
'platform' => array(
'description' => __( 'Platform to track.', 'woocommerce' ),
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
'version' => array(
'description' => __( 'Platform version to track.', 'woocommerce' ),
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
'installation_date' => array(
'description' => __( 'Installation date of the WooCommerce mobile app.', 'woocommerce' ),
'required' => false, // For backward compatibility.
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
),
);
}
}
Controllers/Version1/class-wc-rest-coupons-v1-controller.php 0000644 00000047416 15154275220 0020170 0 ustar 00 <?php
/**
* REST API Coupons controller
*
* Handles requests to the /coupons endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
use Automattic\WooCommerce\Utilities\StringUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Coupons controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Posts_Controller
*/
class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'coupons';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_coupon';
/**
* Coupons actions.
*/
public function __construct() {
add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
}
/**
* Register the routes for coupons.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'required' => true,
'type' => 'string',
),
) ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Query args.
*
* @param array $args Query args
* @param WP_REST_Request $request Request data.
* @return array
*/
public function query_args( $args, $request ) {
$coupon_code = $request['code'] ?? null;
if ( ! StringUtil::is_null_or_whitespace( $coupon_code ) ) {
$id = wc_get_coupon_id_by_code( $coupon_code );
$args['post__in'] = array( $id );
}
return $args;
}
/**
* Prepare a single coupon output for response.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $data
*/
public function prepare_item_for_response( $post, $request ) {
$coupon = new WC_Coupon( (int) $post->ID );
$_data = $coupon->get_data();
$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
$format_date = array( 'date_created', 'date_modified' );
$format_date_utc = array( 'date_expires' );
$format_null = array( 'usage_limit', 'usage_limit_per_user' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
$_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 );
}
// Format date values.
foreach ( $format_date as $key ) {
$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null;
}
foreach ( $format_date_utc as $key ) {
$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null;
}
// Format null values.
foreach ( $format_null as $key ) {
$_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null;
}
$data = array(
'id' => $_data['id'],
'code' => $_data['code'],
'date_created' => $_data['date_created'],
'date_modified' => $_data['date_modified'],
'discount_type' => $_data['discount_type'],
'description' => $_data['description'],
'amount' => $_data['amount'],
'expiry_date' => $_data['date_expires'],
'usage_count' => $_data['usage_count'],
'individual_use' => $_data['individual_use'],
'product_ids' => $_data['product_ids'],
'exclude_product_ids' => $_data['excluded_product_ids'],
'usage_limit' => $_data['usage_limit'],
'usage_limit_per_user' => $_data['usage_limit_per_user'],
'limit_usage_to_x_items' => $_data['limit_usage_to_x_items'],
'free_shipping' => $_data['free_shipping'],
'product_categories' => $_data['product_categories'],
'excluded_product_categories' => $_data['excluded_product_categories'],
'exclude_sale_items' => $_data['exclude_sale_items'],
'minimum_amount' => $_data['minimum_amount'],
'maximum_amount' => $_data['maximum_amount'],
'email_restrictions' => $_data['email_restrictions'],
'used_by' => $_data['used_by'],
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $post, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Only return writable props from schema.
* @param array $schema
* @return bool
*/
protected function filter_writable_props( $schema ) {
return empty( $schema['readonly'] );
}
/**
* Prepare a single coupon for create or update.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|stdClass $data Post object.
*/
protected function prepare_item_for_database( $request ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$coupon = new WC_Coupon( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Update to schema to make compatible with CRUD schema.
if ( $request['exclude_product_ids'] ) {
$request['excluded_product_ids'] = $request['exclude_product_ids'];
}
if ( $request['expiry_date'] ) {
$request['date_expires'] = $request['expiry_date'];
}
// Validate required POST fields.
if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) {
if ( StringUtil::is_null_or_whitespace( $request['code'] ?? null ) ) {
return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
}
}
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'code' :
$coupon_code = wc_format_coupon_code( $value );
$id = $coupon->get_id() ? $coupon->get_id() : 0;
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
}
$coupon->set_code( $coupon_code );
break;
case 'description' :
$coupon->set_description( wp_filter_post_kses( $value ) );
break;
case 'expiry_date' :
$coupon->set_date_expires( $value );
break;
default :
if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
$coupon->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filter the query_vars used in `get_items` for the constructed query.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for insertion.
*
* @param WC_Coupon $coupon The coupon object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request );
}
/**
* Create a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$coupon_id = $this->save_coupon( $request );
if ( is_wp_error( $coupon_id ) ) {
return $coupon_id;
}
$post = get_post( $coupon_id );
$this->update_additional_fields_for_object( $post, $request );
$this->add_post_meta_fields( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
return $response;
}
/**
* Update a single coupon.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
try {
$post_id = (int) $request['id'];
if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
$coupon_id = $this->save_coupon( $request );
if ( is_wp_error( $coupon_id ) ) {
return $coupon_id;
}
$post = get_post( $coupon_id );
$this->update_additional_fields_for_object( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Saves a coupon to the database.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|int
*/
protected function save_coupon( $request ) {
try {
$coupon = $this->prepare_item_for_database( $request );
if ( is_wp_error( $coupon ) ) {
return $coupon;
}
$coupon->save();
return $coupon->get_id();
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the Coupon's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the object.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Coupon description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'discount_type' => array(
'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ),
'type' => 'string',
'default' => 'fixed_cart',
'enum' => array_keys( wc_get_coupon_types() ),
'context' => array( 'view', 'edit' ),
),
'amount' => array(
'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'expiry_date' => array(
'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'usage_count' => array(
'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'individual_use' => array(
'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'product_ids' => array(
'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'exclude_product_ids' => array(
'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'usage_limit' => array(
'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'usage_limit_per_user' => array(
'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'limit_usage_to_x_items' => array(
'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'free_shipping' => array(
'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'product_categories' => array(
'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'excluded_product_categories' => array(
'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'exclude_sale_items' => array(
'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'minimum_amount' => array(
'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'maximum_amount' => array(
'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email_restrictions' => array(
'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
),
'used_by' => array(
'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['code'] = array(
'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php 0000644 00000016707 15154275220 0022332 0 ustar 00 <?php
/**
* REST API Customer Downloads controller
*
* Handles requests to the /customers/<customer_id>/downloads endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'customers/(?P<customer_id>[\d]+)/downloads';
/**
* Register the routes for customers.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
'args' => array(
'customer_id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read customers.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$customer = get_user_by( 'id', (int) $request['customer_id'] );
if ( ! $customer ) {
return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all customer downloads.
*
* @param WP_REST_Request $request
* @return array
*/
public function get_items( $request ) {
$downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] );
$data = array();
foreach ( $downloads as $download_data ) {
$download = $this->prepare_item_for_response( (object) $download_data, $request );
$download = $this->prepare_response_for_collection( $download );
$data[] = $download;
}
return rest_ensure_response( $data );
}
/**
* Prepare a single download output for response.
*
* @param stdObject $download Download object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $download, $request ) {
$data = (array) $download;
$data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never';
$data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining'];
// Remove "product_name" since it's new in 3.0.
unset( $data['product_name'] );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $download, $request ) );
/**
* Filter customer download data returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdObject $download Download object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request );
}
/**
* Prepare links for the request.
*
* @param stdClass $download Download object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given customer download.
*/
protected function prepare_links( $download, $request ) {
$base = str_replace( '(?P<customer_id>[\d]+)', $request['customer_id'], $this->rest_base );
$links = array(
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'product' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ),
),
'order' => array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ),
),
);
return $links;
}
/**
* Get the Customer Download's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'customer_download',
'type' => 'object',
'properties' => array(
'download_url' => array(
'description' => __( 'Download file URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'download_id' => array(
'description' => __( 'Download ID (MD5).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Downloadable product ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'download_name' => array(
'description' => __( 'Downloadable file name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'order_id' => array(
'description' => __( 'Order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'order_key' => array(
'description' => __( 'Order key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'downloads_remaining' => array(
'description' => __( 'Number of downloads remaining.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'access_expires' => array(
'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'file' => array(
'description' => __( 'File details.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-customers-v1-controller.php 0000644 00000076007 15154275220 0020524 0 ustar 00 <?php
/**
* REST API Customers controller
*
* Handles requests to the /customers endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Customers_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'customers';
/**
* Register the routes for customers.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'email' => array(
'required' => true,
'type' => 'string',
'description' => __( 'New user email address.', 'woocommerce' ),
),
'username' => array(
'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ),
'description' => __( 'New user username.', 'woocommerce' ),
'type' => 'string',
),
'password' => array(
'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ),
'description' => __( 'New user password.', 'woocommerce' ),
'type' => 'string',
),
) ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
'reassign' => array(
'default' => 0,
'type' => 'integer',
'description' => __( 'ID to reassign posts to.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Check whether a given request has permission to read customers.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_user_permissions( 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access create customers.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_user_permissions( 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a customer.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$id = (int) $request['id'];
if ( ! wc_rest_check_user_permissions( 'read', $id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access update a customer.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function update_item_permissions_check( $request ) {
$id = (int) $request['id'];
if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access delete a customer.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$id = (int) $request['id'];
if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_user_permissions( 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all customers.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$prepared_args = array();
$prepared_args['exclude'] = $request['exclude'];
$prepared_args['include'] = $request['include'];
$prepared_args['order'] = $request['order'];
$prepared_args['number'] = $request['per_page'];
if ( ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$orderby_possibles = array(
'id' => 'ID',
'include' => 'include',
'name' => 'display_name',
'registered_date' => 'registered',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
$prepared_args['search'] = $request['search'];
if ( ! empty( $prepared_args['search'] ) ) {
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
}
// Filter by email.
if ( ! empty( $request['email'] ) ) {
$prepared_args['search'] = $request['email'];
$prepared_args['search_columns'] = array( 'user_email' );
}
// Filter by role.
if ( 'all' !== $request['role'] ) {
$prepared_args['role'] = $request['role'];
}
/**
* Filter arguments, before passing to WP_User_Query, when querying users via the REST API.
*
* @see https://developer.wordpress.org/reference/classes/wp_user_query/
*
* @param array $prepared_args Array of arguments for WP_User_Query.
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request );
$query = new WP_User_Query( $prepared_args );
$users = array();
foreach ( $query->results as $user ) {
$data = $this->prepare_item_for_response( $user, $request );
$users[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $users );
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$prepared_args['fields'] = 'ID';
$total_users = $query->get_total();
if ( $total_users < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $prepared_args['number'] );
unset( $prepared_args['offset'] );
$count_query = new WP_User_Query( $prepared_args );
$total_users = $count_query->get_total();
}
$response->header( 'X-WP-Total', (int) $total_users );
$max_pages = ceil( $total_users / $per_page );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Create a single customer.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
try {
if ( ! empty( $request['id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 );
}
// Sets the username.
$request['username'] = ! empty( $request['username'] ) ? $request['username'] : '';
// Sets the password.
$request['password'] = ! empty( $request['password'] ) ? $request['password'] : '';
// Create customer.
$customer = new WC_Customer;
$customer->set_username( $request['username'] );
$customer->set_password( $request['password'] );
$customer->set_email( $request['email'] );
$this->update_customer_meta_fields( $customer, $request );
$customer->save();
if ( ! $customer->get_id() ) {
throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 );
}
$user_data = get_userdata( $customer->get_id() );
$this->update_additional_fields_for_object( $user_data, $request );
/**
* Fires after a customer is created or updated via the REST API.
*
* @param WP_User $user_data Data used to create the customer.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating customer, false when updating customer.
*/
do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user_data, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) );
return $response;
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get a single customer.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$user_data = get_userdata( $id );
if ( empty( $id ) || empty( $user_data->ID ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$customer = $this->prepare_item_for_response( $user_data, $request );
$response = rest_ensure_response( $customer );
return $response;
}
/**
* Update a single user.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
try {
$id = (int) $request['id'];
$customer = new WC_Customer( $id );
if ( ! $customer->get_id() ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 );
}
if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) {
throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 );
}
if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) {
throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 );
}
// Customer email.
if ( isset( $request['email'] ) ) {
$customer->set_email( sanitize_email( $request['email'] ) );
}
// Customer password.
if ( isset( $request['password'] ) ) {
$customer->set_password( $request['password'] );
}
$this->update_customer_meta_fields( $customer, $request );
$customer->save();
$user_data = get_userdata( $customer->get_id() );
$this->update_additional_fields_for_object( $user_data, $request );
if ( ! is_user_member_of_blog( $user_data->ID ) ) {
$user_data->add_role( 'customer' );
}
/**
* Fires after a customer is created or updated via the REST API.
*
* @param WP_User $customer Data used to create the customer.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating customer, false when updating customer.
*/
do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user_data, $request );
$response = rest_ensure_response( $response );
return $response;
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Delete a single customer.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null;
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$user_data = get_userdata( $id );
if ( ! $user_data ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );
}
if ( ! empty( $reassign ) ) {
if ( $reassign === $id || ! get_userdata( $reassign ) ) {
return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $user_data, $request );
/** Include admin customer functions to get access to wp_delete_user() */
require_once ABSPATH . 'wp-admin/includes/user.php';
$customer = new WC_Customer( $id );
if ( ! is_null( $reassign ) ) {
$result = $customer->delete_and_reassign( $reassign );
} else {
$result = $customer->delete();
}
if ( ! $result ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
/**
* Fires after a customer is deleted via the REST API.
*
* @param WP_User $user_data User data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request );
return $response;
}
/**
* Prepare a single customer output for response.
*
* @param WP_User $user_data User object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $user_data, $request ) {
$customer = new WC_Customer( $user_data->ID );
$_data = $customer->get_data();
$last_order = wc_get_customer_last_order( $customer->get_id() );
$format_date = array( 'date_created', 'date_modified' );
// Format date values.
foreach ( $format_date as $key ) {
$_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC.
}
$data = array(
'id' => $_data['id'],
'date_created' => $_data['date_created'],
'date_modified' => $_data['date_modified'],
'email' => $_data['email'],
'first_name' => $_data['first_name'],
'last_name' => $_data['last_name'],
'username' => $_data['username'],
'last_order' => array(
'id' => is_object( $last_order ) ? $last_order->get_id() : null,
'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC.
),
'orders_count' => $customer->get_order_count(),
'total_spent' => $customer->get_total_spent(),
'avatar_url' => $customer->get_avatar_url(),
'billing' => $_data['billing'],
'shipping' => $_data['shipping'],
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $user_data ) );
/**
* Filter customer data returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_User $user_data User object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request );
}
/**
* Update customer meta fields.
*
* @param WC_Customer $customer
* @param WP_REST_Request $request
*/
protected function update_customer_meta_fields( $customer, $request ) {
$schema = $this->get_item_schema();
// Customer first name.
if ( isset( $request['first_name'] ) ) {
$customer->set_first_name( wc_clean( $request['first_name'] ) );
}
// Customer last name.
if ( isset( $request['last_name'] ) ) {
$customer->set_last_name( wc_clean( $request['last_name'] ) );
}
// Customer billing address.
if ( isset( $request['billing'] ) ) {
foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) {
if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) {
$customer->{"set_billing_{$field}"}( $request['billing'][ $field ] );
}
}
}
// Customer shipping address.
if ( isset( $request['shipping'] ) ) {
foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) {
if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) {
$customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] );
}
}
}
}
/**
* Prepare links for the request.
*
* @param WP_User $customer Customer object.
* @return array Links for the given customer.
*/
protected function prepare_links( $customer ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the Customer's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'customer',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'email' => array(
'description' => __( 'The email address for the customer.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'first_name' => array(
'description' => __( 'Customer first name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'last_name' => array(
'description' => __( 'Customer last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'username' => array(
'description' => __( 'Customer login name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_user',
),
),
'password' => array(
'description' => __( 'Customer password.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'last_order' => array(
'description' => __( 'Last order data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
'id' => array(
'description' => __( 'Last order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date' => array(
'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
'orders_count' => array(
'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_spent' => array(
'description' => __( 'Total amount spent.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avatar_url' => array(
'description' => __( 'Avatar URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'billing' => array(
'description' => __( 'List of billing address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping' => array(
'description' => __( 'List of shipping address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get role names.
*
* @return array
*/
protected function get_role_names() {
global $wp_roles;
return array_keys( $wp_roles->role_names );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['context']['default'] = 'view';
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'enum' => array( 'asc', 'desc' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'default' => 'name',
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'enum' => array(
'id',
'include',
'name',
'registered_date',
),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email'] = array(
'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'validate_callback' => 'rest_validate_request_arg',
);
$params['role'] = array(
'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ),
'type' => 'string',
'default' => 'customer',
'enum' => array_merge( array( 'all' ), $this->get_role_names() ),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-order-notes-v1-controller.php 0000644 00000034147 15154275220 0020740 0 ustar 00 <?php
/**
* REST API Order Notes controller
*
* Handles requests to the /orders/<order_id>/notes endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Order Notes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Order_Notes_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'orders/(?P<order_id>[\d]+)/notes';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_order';
/**
* Register the routes for order notes.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'note' => array(
'type' => 'string',
'description' => __( 'Order note content.', 'woocommerce' ),
'required' => true,
),
) ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read order notes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access create order notes.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a order note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'read', $order->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access delete a order note.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $order->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get order notes from an order.
*
* @param WP_REST_Request $request
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$args = array(
'post_id' => $order->get_id(),
'approve' => 'approve',
'type' => 'order_note',
);
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$data = array();
foreach ( $notes as $note ) {
$order_note = $this->prepare_item_for_response( $note, $request );
$order_note = $this->prepare_response_for_collection( $order_note );
$data[] = $order_note;
}
return rest_ensure_response( $data );
}
/**
* Create a single order note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
// Create the note.
$note_id = $order->add_order_note( $request['note'], $request['customer_note'] );
if ( ! $note_id ) {
return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
}
$note = get_comment( $note_id );
$this->update_additional_fields_for_object( $note, $request );
/**
* Fires after a order note is created or updated via the REST API.
*
* @param WP_Comment $note New order note object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_order_note', $note, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $note, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) );
return $response;
}
/**
* Get a single order note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$note = get_comment( $id );
if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$order_note = $this->prepare_item_for_response( $note, $request );
$response = rest_ensure_response( $order_note );
return $response;
}
/**
* Delete a single order note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$note = get_comment( $id );
if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $note, $request );
$result = wc_delete_order_note( $note->comment_ID );
if ( ! $result ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) );
}
/**
* Fires after a order note is deleted or trashed via the REST API.
*
* @param WP_Comment $note The deleted or trashed order note.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request );
return $response;
}
/**
* Prepare a single order note output for response.
*
* @param WP_Comment $note Order note object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $note, $request ) {
$data = array(
'id' => (int) $note->comment_ID,
'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ),
'note' => $note->comment_content,
'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $note ) );
/**
* Filter order note object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $note Order note object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
}
/**
* Prepare links for the request.
*
* @param WP_Comment $note Delivery order_note object.
* @return array Links for the given order note.
*/
protected function prepare_links( $note ) {
$order_id = (int) $note->comment_post_ID;
$base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
),
);
return $links;
}
/**
* Get the Order Notes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order_note',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'note' => array(
'description' => __( 'Order note.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'customer_note' => array(
'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php 0000644 00000042100 15154275220 0021242 0 ustar 00 <?php
/**
* REST API Order Refunds controller
*
* Handles requests to the /orders/<order_id>/refunds endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 2.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Order Refunds controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Orders_V1_Controller
*/
class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_order_refund';
/**
* Order refunds actions.
*/
public function __construct() {
add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' );
add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
}
/**
* Register the routes for order refunds.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => true,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Prepare a single order refund output for response.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function prepare_item_for_response( $post, $request ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order ) {
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
}
$refund = wc_get_order( $post );
if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) {
return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 );
}
$dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] );
$data = array(
'id' => $refund->get_id(),
'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ),
'amount' => wc_format_decimal( $refund->get_amount(), $dp ),
'reason' => $refund->get_reason(),
'line_items' => array(),
);
// Add line items.
foreach ( $refund->get_items() as $item_id => $item ) {
$product = $item->get_product();
$product_id = 0;
$variation_id = 0;
$product_sku = null;
// Check if the product exists.
if ( is_object( $product ) ) {
$product_id = $item->get_product_id();
$variation_id = $item->get_variation_id();
$product_sku = $product->get_sku();
}
$item_meta = array();
$hideprefix = 'true' === $request['all_item_meta'] ? null : '_';
foreach ( $item->get_all_formatted_meta_data( $hideprefix ) as $meta_key => $formatted_meta ) {
$item_meta[] = array(
'key' => $formatted_meta->key,
'label' => $formatted_meta->display_key,
'value' => wc_clean( $formatted_meta->display_value ),
);
}
$line_item = array(
'id' => $item_id,
'name' => $item['name'],
'sku' => $product_sku,
'product_id' => (int) $product_id,
'variation_id' => (int) $variation_id,
'quantity' => wc_stock_amount( $item['qty'] ),
'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '',
'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ),
'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ),
'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ),
'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ),
'total_tax' => wc_format_decimal( $item['line_tax'], $dp ),
'taxes' => array(),
'meta' => $item_meta,
);
$item_line_taxes = maybe_unserialize( $item['line_tax_data'] );
if ( isset( $item_line_taxes['total'] ) ) {
$line_tax = array();
foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) {
$line_tax[ $tax_rate_id ] = array(
'id' => $tax_rate_id,
'total' => $tax,
'subtotal' => '',
);
}
foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
$line_tax[ $tax_rate_id ]['subtotal'] = $tax;
}
$line_item['taxes'] = array_values( $line_tax );
}
$data['line_items'][] = $line_item;
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $refund, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Order_Refund $refund Comment object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given order refund.
*/
protected function prepare_links( $refund, $request ) {
$order_id = $refund->get_parent_id();
$base = str_replace( '(?P<order_id>[\d]+)', $order_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ),
),
);
return $links;
}
/**
* Query args.
*
* @param array $args Request args.
* @param WP_REST_Request $request Request object.
* @return array
*/
public function query_args( $args, $request ) {
$args['post_status'] = array_keys( wc_get_order_statuses() );
$args['post_parent__in'] = array( absint( $request['order_id'] ) );
return $args;
}
/**
* Create a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$order_data = get_post( (int) $request['order_id'] );
if ( empty( $order_data ) ) {
return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 );
}
if ( 0 > $request['amount'] ) {
return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
}
// Create the refund.
$refund = wc_create_refund( array(
'order_id' => $order_data->ID,
'amount' => $request['amount'],
'reason' => empty( $request['reason'] ) ? null : $request['reason'],
'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
'restock_items' => true,
) );
if ( is_wp_error( $refund ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
}
if ( ! $refund ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
}
$post = get_post( $refund->get_id() );
$this->update_additional_fields_for_object( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
return $response;
}
/**
* Get the Order's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'amount' => array(
'description' => __( 'Refund amount.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'reason' => array(
'description' => __( 'Reason for refund.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'line_items' => array(
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Product SKU.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'variation_id' => array(
'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'quantity' => array(
'description' => __( 'Quantity ordered.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tax_class' => array(
'description' => __( 'Tax class of product.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'price' => array(
'description' => __( 'Product price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal_tax' => array(
'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'meta' => array(
'description' => __( 'Line item meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Meta label.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['dp'] = array(
'default' => wc_get_price_decimals(),
'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-orders-v1-controller.php 0000644 00000160657 15154275220 0020003 0 ustar 00 <?php
/**
* REST API Orders controller
*
* Handles requests to the /orders endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Posts_Controller
*/
class WC_REST_Orders_V1_Controller extends WC_REST_Posts_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'orders';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_order';
/**
* Initialize orders actions.
*/
public function __construct() {
add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
}
/**
* Register the routes for orders.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Prepare a single order output for response.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $data
*/
public function prepare_item_for_response( $post, $request ) {
$order = wc_get_order( $post );
$dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] );
$data = array(
'id' => $order->get_id(),
'parent_id' => $order->get_parent_id(),
'status' => $order->get_status(),
'order_key' => $order->get_order_key(),
'number' => $order->get_order_number(),
'currency' => $order->get_currency(),
'version' => $order->get_version(),
'prices_include_tax' => $order->get_prices_include_tax(),
'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC.
'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC.
'customer_id' => $order->get_customer_id(),
'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ),
'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ),
'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ),
'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ),
'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ),
'total' => wc_format_decimal( $order->get_total(), $dp ),
'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ),
'billing' => array(),
'shipping' => array(),
'payment_method' => $order->get_payment_method(),
'payment_method_title' => $order->get_payment_method_title(),
'transaction_id' => $order->get_transaction_id(),
'customer_ip_address' => $order->get_customer_ip_address(),
'customer_user_agent' => $order->get_customer_user_agent(),
'created_via' => $order->get_created_via(),
'customer_note' => $order->get_customer_note(),
'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time.
'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time.
'cart_hash' => $order->get_cart_hash(),
'line_items' => array(),
'tax_lines' => array(),
'shipping_lines' => array(),
'fee_lines' => array(),
'coupon_lines' => array(),
'refunds' => array(),
);
// Add addresses.
$data['billing'] = $order->get_address( 'billing' );
$data['shipping'] = $order->get_address( 'shipping' );
// Add line items.
foreach ( $order->get_items() as $item_id => $item ) {
$product = $item->get_product();
$product_id = 0;
$variation_id = 0;
$product_sku = null;
// Check if the product exists.
if ( is_object( $product ) ) {
$product_id = $item->get_product_id();
$variation_id = $item->get_variation_id();
$product_sku = $product->get_sku();
}
$item_meta = array();
$hideprefix = 'true' === $request['all_item_meta'] ? null : '_';
foreach ( $item->get_all_formatted_meta_data( $hideprefix ) as $meta_key => $formatted_meta ) {
$item_meta[] = array(
'key' => $formatted_meta->key,
'label' => $formatted_meta->display_key,
'value' => wc_clean( $formatted_meta->display_value ),
);
}
$line_item = array(
'id' => $item_id,
'name' => $item['name'],
'sku' => $product_sku,
'product_id' => (int) $product_id,
'variation_id' => (int) $variation_id,
'quantity' => wc_stock_amount( $item['qty'] ),
'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '',
'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ),
'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ),
'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ),
'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ),
'total_tax' => wc_format_decimal( $item['line_tax'], $dp ),
'taxes' => array(),
'meta' => $item_meta,
);
$item_line_taxes = maybe_unserialize( $item['line_tax_data'] );
if ( isset( $item_line_taxes['total'] ) ) {
$line_tax = array();
foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) {
$line_tax[ $tax_rate_id ] = array(
'id' => $tax_rate_id,
'total' => $tax,
'subtotal' => '',
);
}
foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
$line_tax[ $tax_rate_id ]['subtotal'] = $tax;
}
$line_item['taxes'] = array_values( $line_tax );
}
$data['line_items'][] = $line_item;
}
// Add taxes.
foreach ( $order->get_items( 'tax' ) as $key => $tax ) {
$tax_line = array(
'id' => $key,
'rate_code' => $tax['name'],
'rate_id' => $tax['rate_id'],
'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'],
'compound' => (bool) $tax['compound'],
'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ),
'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ),
);
$data['tax_lines'][] = $tax_line;
}
// Add shipping.
foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) {
$shipping_line = array(
'id' => $shipping_item_id,
'method_title' => $shipping_item['name'],
'method_id' => $shipping_item['method_id'],
'total' => wc_format_decimal( $shipping_item['cost'], $dp ),
'total_tax' => wc_format_decimal( '', $dp ),
'taxes' => array(),
);
$shipping_taxes = $shipping_item->get_taxes();
if ( ! empty( $shipping_taxes['total'] ) ) {
$shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp );
foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) {
$shipping_line['taxes'][] = array(
'id' => $tax_rate_id,
'total' => $tax,
);
}
}
$data['shipping_lines'][] = $shipping_line;
}
// Add fees.
foreach ( $order->get_fees() as $fee_item_id => $fee_item ) {
$fee_line = array(
'id' => $fee_item_id,
'name' => $fee_item['name'],
'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '',
'tax_status' => 'taxable',
'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ),
'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ),
'taxes' => array(),
);
$fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] );
if ( isset( $fee_line_taxes['total'] ) ) {
$fee_tax = array();
foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) {
$fee_tax[ $tax_rate_id ] = array(
'id' => $tax_rate_id,
'total' => $tax,
'subtotal' => '',
);
}
if ( isset( $fee_line_taxes['subtotal'] ) ) {
foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) {
$fee_tax[ $tax_rate_id ]['subtotal'] = $tax;
}
}
$fee_line['taxes'] = array_values( $fee_tax );
}
$data['fee_lines'][] = $fee_line;
}
// Add coupons.
foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) {
$coupon_line = array(
'id' => $coupon_item_id,
'code' => $coupon_item['name'],
'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ),
'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ),
);
$data['coupon_lines'][] = $coupon_line;
}
// Add refunds.
foreach ( $order->get_refunds() as $refund ) {
$data['refunds'][] = array(
'id' => $refund->get_id(),
'refund' => $refund->get_reason() ? $refund->get_reason() : '',
'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $order, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Order $order Order object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given order.
*/
protected function prepare_links( $order, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( 0 !== (int) $order->get_user_id() ) {
$links['customer'] = array(
'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ),
);
}
if ( 0 !== (int) $order->get_parent_id() ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ),
);
}
return $links;
}
/**
* Query args.
*
* @param array $args
* @param WP_REST_Request $request
* @return array
*/
public function query_args( $args, $request ) {
global $wpdb;
// Set post_status.
if ( 'any' !== $request['status'] ) {
$args['post_status'] = 'wc-' . $request['status'];
} else {
$args['post_status'] = 'any';
}
if ( isset( $request['customer'] ) ) {
if ( ! empty( $args['meta_query'] ) ) {
$args['meta_query'] = array();
}
$args['meta_query'][] = array(
'key' => '_customer_user',
'value' => $request['customer'],
'type' => 'NUMERIC',
);
}
// Search by product.
if ( ! empty( $request['product'] ) ) {
$order_ids = $wpdb->get_col( $wpdb->prepare( "
SELECT order_id
FROM {$wpdb->prefix}woocommerce_order_items
WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
AND order_item_type = 'line_item'
", $request['product'] ) );
// Force WP_Query return empty if don't found any order.
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
$args['post__in'] = $order_ids;
}
// Search.
if ( ! empty( $args['s'] ) ) {
$order_ids = wc_order_search( $args['s'] );
if ( ! empty( $order_ids ) ) {
unset( $args['s'] );
$args['post__in'] = array_merge( $order_ids, array( 0 ) );
}
}
return $args;
}
/**
* Prepare a single order for create.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|WC_Order $data Object.
*/
protected function prepare_item_for_database( $request ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$order = new WC_Order( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Handle all writable props
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'billing' :
case 'shipping' :
$this->update_address( $order, $value, $key );
break;
case 'line_items' :
case 'shipping_lines' :
case 'fee_lines' :
case 'coupon_lines' :
if ( is_array( $value ) ) {
foreach ( $value as $item ) {
if ( is_array( $item ) ) {
if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
$order->remove_item( $item['id'] );
} else {
$this->set_item( $order, $key, $item );
}
}
}
}
break;
default :
if ( is_callable( array( $order, "set_{$key}" ) ) ) {
$order->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filter the data for the insert.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WC_Order $order The order object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request );
}
/**
* Create base WC Order object.
* @deprecated 3.0.0
* @param array $data
* @return WC_Order
*/
protected function create_base_order( $data ) {
return wc_create_order( $data );
}
/**
* Only return writable props from schema.
* @param array $schema
* @return bool
*/
protected function filter_writable_props( $schema ) {
return empty( $schema['readonly'] );
}
/**
* Create order.
*
* @param WP_REST_Request $request Full details about the request.
* @return int|WP_Error
*/
protected function create_order( $request ) {
try {
// Make sure customer exists.
if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 );
}
// Make sure customer is part of blog.
if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
}
$order = $this->prepare_item_for_database( $request );
$order->set_created_via( 'rest-api' );
$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
$order->calculate_totals();
$order->save();
// Handle set paid.
if ( true === $request['set_paid'] ) {
$order->payment_complete( $request['transaction_id'] );
}
return $order->get_id();
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Update order.
*
* @param WP_REST_Request $request Full details about the request.
* @return int|WP_Error
*/
protected function update_order( $request ) {
try {
$order = $this->prepare_item_for_database( $request );
$order->save();
// Handle set paid.
if ( $order->needs_payment() && true === $request['set_paid'] ) {
$order->payment_complete( $request['transaction_id'] );
}
// If items have changed, recalculate order totals.
if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
$order->calculate_totals( true );
}
return $order->get_id();
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Update address.
*
* @param WC_Order $order Order object.
* @param array $posted Request data.
* @param string $type Type of address; 'billing' or 'shipping'.
*/
protected function update_address( $order, $posted, $type = 'billing' ) {
foreach ( $posted as $key => $value ) {
if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
$order->{"set_{$type}_{$key}"}( $value );
}
}
}
/**
* Gets the product ID from the SKU or posted ID.
*
* @throws WC_REST_Exception When SKU or ID is not valid.
* @param array $posted Request data.
* @param string $action 'create' to add line item or 'update' to update it.
* @return int
*/
protected function get_product_id( $posted, $action = 'create' ) {
if ( ! empty( $posted['sku'] ) ) {
$product_id = (int) wc_get_product_id_by_sku( $posted['sku'] );
} elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) {
$product_id = (int) $posted['product_id'];
} elseif ( ! empty( $posted['variation_id'] ) ) {
$product_id = (int) $posted['variation_id'];
} elseif ( 'update' === $action ) {
$product_id = 0;
} else {
throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 );
}
return $product_id;
}
/**
* Maybe set an item prop if the value was posted.
* @param WC_Order_Item $item
* @param string $prop
* @param array $posted Request data.
*/
protected function maybe_set_item_prop( $item, $prop, $posted ) {
if ( isset( $posted[ $prop ] ) ) {
$item->{"set_$prop"}( $posted[ $prop ] );
}
}
/**
* Maybe set item props if the values were posted.
* @param WC_Order_Item $item
* @param string[] $props
* @param array $posted Request data.
*/
protected function maybe_set_item_props( $item, $props, $posted ) {
foreach ( $props as $prop ) {
$this->maybe_set_item_prop( $item, $prop, $posted );
}
}
/**
* Create or update a line item.
*
* @param array $posted Line item data.
* @param string $action 'create' to add line item or 'update' to update it.
*
* @return WC_Order_Item_Product
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_line_items( $posted, $action = 'create' ) {
$item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' );
$product = wc_get_product( $this->get_product_id( $posted, $action ) );
if ( $product && $product !== $item->get_product() ) {
$item->set_product( $product );
if ( 'create' === $action ) {
$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
$total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
$item->set_total( $total );
$item->set_subtotal( $total );
}
}
$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
return $item;
}
/**
* Create or update an order shipping method.
*
* @param $posted $shipping Item data.
* @param string $action 'create' to add shipping or 'update' to update it.
*
* @return WC_Order_Item_Shipping
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_shipping_lines( $posted, $action ) {
$item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' );
if ( 'create' === $action ) {
if ( empty( $posted['method_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted );
return $item;
}
/**
* Create or update an order fee.
*
* @param array $posted Item data.
* @param string $action 'create' to add fee or 'update' to update it.
*
* @return WC_Order_Item_Fee
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_fee_lines( $posted, $action ) {
$item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' );
if ( 'create' === $action ) {
if ( empty( $posted['name'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted );
return $item;
}
/**
* Create or update an order coupon.
*
* @param array $posted Item data.
* @param string $action 'create' to add coupon or 'update' to update it.
*
* @return WC_Order_Item_Coupon
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_coupon_lines( $posted, $action ) {
$item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' );
if ( 'create' === $action ) {
$coupon_code = ArrayUtil::get_value_or_default( $posted, 'code' );
if ( StringUtil::is_null_or_whitespace( $coupon_code ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted );
return $item;
}
/**
* Wrapper method to create/update order items.
* When updating, the item ID provided is checked to ensure it is associated
* with the order.
*
* @param WC_Order $order order
* @param string $item_type
* @param array $posted item provided in the request body
* @throws WC_REST_Exception If item ID is not associated with order
*/
protected function set_item( $order, $item_type, $posted ) {
global $wpdb;
if ( ! empty( $posted['id'] ) ) {
$action = 'update';
} else {
$action = 'create';
}
$method = 'prepare_' . $item_type;
// Verify provided line item ID is associated with order.
if ( 'update' === $action ) {
$result = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d",
absint( $posted['id'] ),
absint( $order->get_id() )
) );
if ( is_null( $result ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
}
}
// Prepare item data
$item = $this->$method( $posted, $action );
/**
* Action hook to adjust item before save.
* @since 3.0.0
*/
do_action( 'woocommerce_rest_set_order_item', $item, $posted );
// Save or add to order
if ( 'create' === $action ) {
$order->add_item( $item );
} else {
$item->save();
}
}
/**
* Helper method to check if the resource ID associated with the provided item is null.
* Items can be deleted by setting the resource ID to null.
*
* @param array $item Item provided in the request body.
* @return bool True if the item resource ID is null, false otherwise.
*/
protected function item_is_null( $item ) {
$keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' );
foreach ( $keys as $key ) {
if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
return true;
}
}
return false;
}
/**
* Create a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$order_id = $this->create_order( $request );
if ( is_wp_error( $order_id ) ) {
return $order_id;
}
$post = get_post( $order_id );
$this->update_additional_fields_for_object( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
return $response;
}
/**
* Update a single order.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
try {
$post_id = (int) $request['id'];
if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
$order_id = $this->update_order( $request );
if ( is_wp_error( $order_id ) ) {
return $order_id;
}
$post = get_post( $order_id );
$this->update_additional_fields_for_object( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
} catch ( Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get order statuses without prefixes.
* @return array
*/
protected function get_order_statuses() {
$order_statuses = array();
foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
$order_statuses[] = str_replace( 'wc-', '', $status );
}
return $order_statuses;
}
/**
* Check if a given request has access to read an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$object = wc_get_order( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::get_item_permissions_check( $request );
}
/**
* Check if a given request has access to update an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$object = wc_get_order( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::update_item_permissions_check( $request );
}
/**
* Check if a given request has access to delete an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$object = wc_get_order( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::delete_item_permissions_check( $request );
}
/**
* Get the Order's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'parent_id' => array(
'description' => __( 'Parent order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Order status.', 'woocommerce' ),
'type' => 'string',
'default' => 'pending',
'enum' => $this->get_order_statuses(),
'context' => array( 'view', 'edit' ),
),
'order_key' => array(
'description' => __( 'Order key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'number' => array(
'description' => __( 'Order number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'currency' => array(
'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ),
'type' => 'string',
'default' => get_woocommerce_currency(),
'enum' => array_keys( get_woocommerce_currencies() ),
'context' => array( 'view', 'edit' ),
),
'version' => array(
'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'prices_include_tax' => array(
'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order was created, as GMT.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_id' => array(
'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ),
'type' => 'integer',
'default' => 0,
'context' => array( 'view', 'edit' ),
),
'discount_total' => array(
'description' => __( 'Total discount amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'discount_tax' => array(
'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_total' => array(
'description' => __( 'Total shipping amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax' => array(
'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'cart_tax' => array(
'description' => __( 'Sum of line item taxes only.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Grand total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Sum of all taxes.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'billing' => array(
'description' => __( 'Billing address.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping' => array(
'description' => __( 'Shipping address.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'payment_method' => array(
'description' => __( 'Payment method ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'payment_method_title' => array(
'description' => __( 'Payment method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'set_paid' => array(
'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'edit' ),
),
'transaction_id' => array(
'description' => __( 'Unique transaction ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'customer_ip_address' => array(
'description' => __( "Customer's IP address.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_user_agent' => array(
'description' => __( 'User agent of the customer.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'created_via' => array(
'description' => __( 'Shows where the order was created.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_note' => array(
'description' => __( 'Note left by customer during checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_completed' => array(
'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_paid' => array(
'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'cart_hash' => array(
'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'line_items' => array(
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Product SKU.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'variation_id' => array(
'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'quantity' => array(
'description' => __( 'Quantity ordered.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class of product.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'price' => array(
'description' => __( 'Product price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'subtotal_tax' => array(
'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'meta' => array(
'description' => __( 'Line item meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Meta label.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
),
),
'tax_lines' => array(
'description' => __( 'Tax lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rate_code' => array(
'description' => __( 'Tax rate code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rate_id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Tax rate label.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'compound' => array(
'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tax_total' => array(
'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax_total' => array(
'description' => __( 'Shipping tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'shipping_lines' => array(
'description' => __( 'Shipping lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_title' => array(
'description' => __( 'Shipping method name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'method_id' => array(
'description' => __( 'Shipping method ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
),
),
'fee_lines' => array(
'description' => __( 'Fee lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Fee name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class of fee.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status of fee.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'enum' => array( 'taxable', 'none' ),
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
),
),
'coupon_lines' => array(
'description' => __( 'Coupons line data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'discount' => array(
'description' => __( 'Discount total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'discount_tax' => array(
'description' => __( 'Discount total tax.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'refunds' => array(
'description' => __( 'List of refunds.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Refund ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'reason' => array(
'description' => __( 'Refund reason.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Refund total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['status'] = array(
'default' => 'any',
'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customer'] = array(
'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product'] = array(
'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['dp'] = array(
'default' => wc_get_price_decimals(),
'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php 0000644 00000017117 15154275220 0023306 0 ustar 00 <?php
/**
* REST API Product Attribute Terms controller
*
* Handles requests to the products/attributes/<attribute_id>/terms endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Attribute Terms controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Terms_Controller
*/
class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/attributes/(?P<attribute_id>[\d]+)/terms';
/**
* Register the routes for terms.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base,
array(
'args' => array(
'attribute_id' => array(
'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'name' => array(
'type' => 'string',
'description' => __( 'Name for the resource.', 'woocommerce' ),
'required' => true,
),
) ),
),
'schema' => array( $this, 'get_public_item_schema' ),
));
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
'attribute_id' => array(
'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
'args' => array(
'attribute_id' => array(
'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Prepare a single product attribute term output for response.
*
* @param WP_Term $item Term object.
* @param WP_REST_Request $request
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
// Get term order.
$menu_order = get_term_meta( $item->term_id, 'order_' . $this->taxonomy, true );
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'description' => $item->description,
'menu_order' => (int) $menu_order,
'count' => (int) $item->count,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Update term meta fields.
*
* @param WP_Term $term
* @param WP_REST_Request $request
* @return bool|WP_Error
*/
protected function update_term_meta_fields( $term, $request ) {
$id = (int) $term->term_id;
update_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] );
return true;
}
/**
* Get the Attribute Term's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_attribute_term',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php 0000644 00000046751 15154275220 0022347 0 ustar 00 <?php
/**
* REST API Product Attributes controller
*
* Handles requests to the products/attributes endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Attributes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Product_Attributes_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/attributes';
/**
* Attribute name.
*
* @var string
*/
protected $attribute = '';
/**
* Cached taxonomies by attribute id.
*
* @var array
*/
protected $taxonomies_by_id = array();
/**
* Register the routes for product attributes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
array(
'name' => array(
'description' => __( 'Name for the resource.', 'woocommerce' ),
'type' => 'string',
'required' => true,
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => true,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check if a given request has access to read the attributes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create a attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! $this->get_taxonomy( $request ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update a attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! $this->get_taxonomy( $request ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete a attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
if ( ! $this->get_taxonomy( $request ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all attributes.
*
* @param WP_REST_Request $request The request to get the attributes from.
* @return array
*/
public function get_items( $request ) {
$attributes = wc_get_attribute_taxonomies();
$data = array();
foreach ( $attributes as $attribute_obj ) {
$attribute = $this->prepare_item_for_response( $attribute_obj, $request );
$attribute = $this->prepare_response_for_collection( $attribute );
$data[] = $attribute;
}
$response = rest_ensure_response( $data );
// This API call always returns all product attributes due to retrieval from the object cache.
$response->header( 'X-WP-Total', count( $data ) );
$response->header( 'X-WP-TotalPages', 1 );
return $response;
}
/**
* Create a single attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
global $wpdb;
$id = wc_create_attribute(
array(
'name' => $request['name'],
'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
'type' => ! empty( $request['type'] ) ? $request['type'] : 'select',
'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
'has_archives' => true === $request['has_archives'],
)
);
// Checks for errors.
if ( is_wp_error( $id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
}
$attribute = $this->get_attribute( $id );
if ( is_wp_error( $attribute ) ) {
return $attribute;
}
$this->update_additional_fields_for_object( $attribute, $request );
/**
* Fires after a single product attribute is created or updated via the REST API.
*
* @param stdObject $attribute Inserted attribute object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating attribute, false when updating.
*/
do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attribute, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );
return $response;
}
/**
* Get a single attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function get_item( $request ) {
$attribute = $this->get_attribute( (int) $request['id'] );
if ( is_wp_error( $attribute ) ) {
return $attribute;
}
$response = $this->prepare_item_for_response( $attribute, $request );
return rest_ensure_response( $response );
}
/**
* Update a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function update_item( $request ) {
global $wpdb;
$id = (int) $request['id'];
$edited = wc_update_attribute(
$id,
array(
'name' => $request['name'],
'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ),
'type' => $request['type'],
'order_by' => $request['order_by'],
'has_archives' => $request['has_archives'],
)
);
// Checks for errors.
if ( is_wp_error( $edited ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) );
}
$attribute = $this->get_attribute( $id );
if ( is_wp_error( $attribute ) ) {
return $attribute;
}
$this->update_additional_fields_for_object( $attribute, $request );
/**
* Fires after a single product attribute is created or updated via the REST API.
*
* @param stdObject $attribute Inserted attribute object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating attribute, false when updating.
*/
do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attribute, $request );
return rest_ensure_response( $response );
}
/**
* Delete a single attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$attribute = $this->get_attribute( (int) $request['id'] );
if ( is_wp_error( $attribute ) ) {
return $attribute;
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attribute, $request );
$deleted = wc_delete_attribute( $attribute->attribute_id );
if ( false === $deleted ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
/**
* Fires after a single attribute is deleted via the REST API.
*
* @param stdObject $attribute The deleted attribute.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request );
return $response;
}
/**
* Prepare a single product attribute output for response.
*
* @param obj $item Term object.
* @param WP_REST_Request $request The request to process.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
$data = array(
'id' => (int) $item->attribute_id,
'name' => $item->attribute_label,
'slug' => wc_attribute_taxonomy_name( $item->attribute_name ),
'type' => $item->attribute_type,
'order_by' => $item->attribute_orderby,
'has_archives' => (bool) $item->attribute_public,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter a attribute item returned from the API.
*
* Allows modification of the product attribute data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original attribute object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $attribute Attribute object.
* @return array Links for the given attribute.
*/
protected function prepare_links( $attribute ) {
$base = '/' . $this->namespace . '/' . $this->rest_base;
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ),
),
'collection' => array(
'href' => rest_url( $base ),
),
);
return $links;
}
/**
* Get the Attribute's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_attribute',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'type' => array(
'description' => __( 'Type of attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'select',
'enum' => array_keys( wc_get_attribute_types() ),
'context' => array( 'view', 'edit' ),
),
'order_by' => array(
'description' => __( 'Default sort order.', 'woocommerce' ),
'type' => 'string',
'default' => 'menu_order',
'enum' => array( 'menu_order', 'name', 'name_num', 'id' ),
'context' => array( 'view', 'edit' ),
),
'has_archives' => array(
'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
return $params;
}
/**
* Get attribute name.
*
* @param WP_REST_Request $request Full details about the request.
* @return string
*/
protected function get_taxonomy( $request ) {
$attribute_id = $request['id'];
if ( empty( $attribute_id ) ) {
return '';
}
if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) {
return $this->taxonomies_by_id[ $attribute_id ];
}
$taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['id'] );
if ( ! empty( $taxonomy ) ) {
$this->taxonomies_by_id[ $attribute_id ] = $taxonomy;
}
return $taxonomy;
}
/**
* Get attribute data.
*
* @param int $id Attribute ID.
* @return stdClass|WP_Error
*/
protected function get_attribute( $id ) {
global $wpdb;
$attribute = $wpdb->get_row(
$wpdb->prepare(
"
SELECT *
FROM {$wpdb->prefix}woocommerce_attribute_taxonomies
WHERE attribute_id = %d
",
$id
)
);
if ( is_wp_error( $attribute ) || is_null( $attribute ) ) {
return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $attribute;
}
/**
* Validate attribute slug.
*
* @deprecated 3.2.0
* @param string $slug The slug to validate.
* @param bool $new_data If we are creating new data.
* @return bool|WP_Error
*/
protected function validate_attribute_slug( $slug, $new_data = true ) {
if ( strlen( $slug ) > 28 ) {
/* translators: %s: slug being validated */
return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
} elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) {
/* translators: %s: slug being validated */
return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
} elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) {
/* translators: %s: slug being validated */
return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) );
}
return true;
}
/**
* Schedule to flush rewrite rules.
*
* @deprecated 3.2.0
* @since 3.0.0
*/
protected function flush_rewrite_rules() {
wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' );
}
}
Controllers/Version1/class-wc-rest-product-categories-v1-controller.php 0000644 00000020460 15154275220 0022273 0 ustar 00 <?php
/**
* REST API Product Categories controller
*
* Handles requests to the products/categories endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Categories controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Terms_Controller
*/
class WC_REST_Product_Categories_V1_Controller extends WC_REST_Terms_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/categories';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = 'product_cat';
/**
* Prepare a single product category output for response.
*
* @param WP_Term $item Term object.
* @param WP_REST_Request $request Request instance.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
// Get category display type.
$display_type = get_term_meta( $item->term_id, 'display_type', true );
// Get category order.
$menu_order = get_term_meta( $item->term_id, 'order', true );
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'parent' => (int) $item->parent,
'description' => $item->description,
'display' => $display_type ? $display_type : 'default',
'image' => null,
'menu_order' => (int) $menu_order,
'count' => (int) $item->count,
);
// Get category image.
$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
if ( $image_id ) {
$attachment = get_post( $image_id );
$data['image'] = array(
'id' => (int) $image_id,
'date_created' => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
'src' => wp_get_attachment_url( $image_id ),
'title' => get_the_title( $attachment ),
'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Update term meta fields.
*
* @param WP_Term $term Term object.
* @param WP_REST_Request $request Request instance.
* @return bool|WP_Error
*/
protected function update_term_meta_fields( $term, $request ) {
$id = (int) $term->term_id;
if ( isset( $request['display'] ) ) {
update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] );
}
if ( isset( $request['menu_order'] ) ) {
update_term_meta( $id, 'order', $request['menu_order'] );
}
if ( isset( $request['image'] ) ) {
if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) );
if ( is_wp_error( $upload ) ) {
return $upload;
}
$image_id = wc_rest_set_uploaded_image_as_attachment( $upload );
} else {
$image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0;
}
// Check if image_id is a valid image attachment before updating the term meta.
if ( $image_id && wp_attachment_is_image( $image_id ) ) {
update_term_meta( $id, 'thumbnail_id', $image_id );
// Set the image alt.
if ( ! empty( $request['image']['alt'] ) ) {
update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) );
}
// Set the image title.
if ( ! empty( $request['image']['title'] ) ) {
wp_update_post( array(
'ID' => $image_id,
'post_title' => wc_clean( $request['image']['title'] ),
) );
}
} else {
delete_term_meta( $id, 'thumbnail_id' );
}
}
return true;
}
/**
* Get the Category schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'parent' => array(
'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'display' => array(
'description' => __( 'Category archive display type.', 'woocommerce' ),
'type' => 'string',
'default' => 'default',
'enum' => array( 'default', 'products', 'subcategories', 'both' ),
'context' => array( 'view', 'edit' ),
),
'image' => array(
'description' => __( 'Image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'title' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php 0000644 00000047442 15154275220 0021643 0 ustar 00 <?php
/**
* REST API Product Reviews Controller
*
* Handles requests to /products/<product_id>/reviews.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Reviews Controller Class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews';
/**
* Register the routes for product reviews.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'review' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Review content.', 'woocommerce' ),
),
'name' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Name of the reviewer.', 'woocommerce' ),
),
'email' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Email of the reviewer.', 'woocommerce' ),
),
) ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read webhook deliveries.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create a new product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all reviews from a product.
*
* @param WP_REST_Request $request
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$reviews = get_approved_comments( $product_id );
$data = array();
foreach ( $reviews as $review_data ) {
$review = $this->prepare_item_for_response( $review_data, $request );
$review = $this->prepare_response_for_collection( $review );
$data[] = $review;
}
return rest_ensure_response( $data );
}
/**
* Get a single product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$review = get_comment( $id );
if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$delivery = $this->prepare_item_for_response( $review, $request );
$response = rest_ensure_response( $delivery );
return $response;
}
/**
* Create a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$prepared_review = $this->prepare_item_for_database( $request );
/**
* Filter a product review (comment) before it is inserted via the REST API.
*
* Allows modification of the comment right before it is inserted via `wp_insert_comment`.
*
* @param array $prepared_review The prepared comment data for `wp_insert_comment`.
* @param WP_REST_Request $request Request used to insert the comment.
*/
$prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request );
$product_review_id = wp_insert_comment( $prepared_review );
if ( ! $product_review_id ) {
return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) );
$product_review = get_comment( $product_review_id );
$this->update_additional_fields_for_object( $product_review, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Comment $product_review Inserted object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $product_review, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) );
return $response;
}
/**
* Update a single product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$product_review_id = (int) $request['id'];
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$review = get_comment( $product_review_id );
if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) {
return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$prepared_review = $this->prepare_item_for_database( $request );
$updated = wp_update_comment( $prepared_review );
if ( 0 === $updated ) {
return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
if ( ! empty( $request['rating'] ) ) {
update_comment_meta( $product_review_id, 'rating', $request['rating'] );
}
$product_review = get_comment( $product_review_id );
$this->update_additional_fields_for_object( $product_review, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Comment $comment Inserted object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $product_review, $request );
return rest_ensure_response( $response );
}
/**
* Delete a product review.
*
* @param WP_REST_Request $request Full details about the request
*
* @return bool|WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
$product_id = (int) $request['product_id'];
$product_review_id = (int) $request['id'];
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$product_review = get_comment( $product_review_id );
if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) {
return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
/**
* Filter whether a product review is trashable.
*
* Return false to disable trash support for the product review.
*
* @param boolean $supports_trash Whether the object supports trashing.
* @param WP_Post $product_review The object being considered for trashing support.
*/
$supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $product_review, $request );
if ( $force ) {
$result = wp_delete_comment( $product_review_id, true );
} else {
if ( ! $supports_trash ) {
return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
if ( 'trash' === $product_review->comment_approved ) {
return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
}
$result = wp_trash_comment( $product_review->comment_ID );
}
if ( ! $result ) {
return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
/**
* Fires after a product review is deleted via the REST API.
*
* @param object $product_review The deleted item.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_product_review', $product_review, $response, $request );
return $response;
}
/**
* Prepare a single product review output for response.
*
* @param WP_Comment $review Product review object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $review, $request ) {
$data = array(
'id' => (int) $review->comment_ID,
'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ),
'review' => $review->comment_content,
'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ),
'name' => $review->comment_author,
'email' => $review->comment_author_email,
'verified' => wc_review_is_from_verified_owner( $review->comment_ID ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $review, $request ) );
/**
* Filter product reviews object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $review Product review object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
}
/**
* Prepare a single product review to be inserted into the database.
*
* @param WP_REST_Request $request Request object.
* @return array|WP_Error $prepared_review
*/
protected function prepare_item_for_database( $request ) {
$prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' );
if ( isset( $request['id'] ) ) {
$prepared_review['comment_ID'] = (int) $request['id'];
}
if ( isset( $request['review'] ) ) {
$prepared_review['comment_content'] = $request['review'];
}
if ( isset( $request['product_id'] ) ) {
$prepared_review['comment_post_ID'] = (int) $request['product_id'];
}
if ( isset( $request['name'] ) ) {
$prepared_review['comment_author'] = $request['name'];
}
if ( isset( $request['email'] ) ) {
$prepared_review['comment_author_email'] = $request['email'];
}
if ( isset( $request['date_created'] ) ) {
$prepared_review['comment_date'] = $request['date_created'];
}
if ( isset( $request['date_created_gmt'] ) ) {
$prepared_review['comment_date_gmt'] = $request['date_created_gmt'];
}
return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request );
}
/**
* Prepare links for the request.
*
* @param WP_Comment $review Product review object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given product review.
*/
protected function prepare_links( $review, $request ) {
$product_id = (int) $request['product_id'];
$base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
),
);
return $links;
}
/**
* Get the Product Review's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_review',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'review' => array(
'description' => __( 'The content of the review.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'rating' => array(
'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Reviewer name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Reviewer email.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'verified' => array(
'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php 0000644 00000007217 15154275220 0023427 0 ustar 00 <?php
/**
* REST API Product Shipping Classes controller
*
* Handles requests to the products/shipping_classes endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Shipping Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Terms_Controller
*/
class WC_REST_Product_Shipping_Classes_V1_Controller extends WC_REST_Terms_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/shipping_classes';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = 'product_shipping_class';
/**
* Prepare a single product shipping class output for response.
*
* @param obj $item Term object.
* @param WP_REST_Request $request
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'description' => $item->description,
'count' => (int) $item->count,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Get the Shipping Class schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Shipping class name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version1/class-wc-rest-product-tags-v1-controller.php 0000644 00000007051 15154275220 0021105 0 ustar 00 <?php
/**
* REST API Product Tags controller
*
* Handles requests to the products/tags endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Product Tags controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Terms_Controller
*/
class WC_REST_Product_Tags_V1_Controller extends WC_REST_Terms_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/tags';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = 'product_tag';
/**
* Prepare a single product tag output for response.
*
* @param obj $item Term object.
* @param WP_REST_Request $request
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'description' => $item->description,
'count' => (int) $item->count,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Get the Tag's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Tag name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version1/class-wc-rest-products-v1-controller.php 0000644 00000262152 15154275220 0020341 0 ustar 00 <?php
/**
* REST API Products controller
*
* Handles requests to the /products endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Products controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Posts_Controller
*/
class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'product';
/**
* Initialize product actions.
*/
public function __construct() {
add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 );
add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) );
}
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
'type' => 'boolean',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Get post types.
*
* @return array
*/
protected function get_post_types() {
return array( 'product', 'product_variation' );
}
/**
* Query args.
*
* @param array $args Request args.
* @param WP_REST_Request $request Request data.
* @return array
*/
public function query_args( $args, $request ) {
// Set post_status.
$args['post_status'] = $request['status'];
// Taxonomy query to filter products by type, category,
// tag, shipping class, and attribute.
$tax_query = array();
// Map between taxonomy name and arg's key.
$taxonomies = array(
'product_cat' => 'category',
'product_tag' => 'tag',
'product_shipping_class' => 'shipping_class',
);
// Set tax_query for each passed arg.
foreach ( $taxonomies as $taxonomy => $key ) {
if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) {
$request[ $key ] = array_filter( $request[ $key ] );
}
if ( ! empty( $request[ $key ] ) ) {
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $request[ $key ],
);
}
}
// Filter product type by slug.
if ( ! empty( $request['type'] ) ) {
$tax_query[] = array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $request['type'],
);
}
// Filter by attribute and term.
if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
$tax_query[] = array(
'taxonomy' => $request['attribute'],
'field' => 'term_id',
'terms' => $request['attribute_term'],
);
}
}
if ( ! empty( $tax_query ) ) {
$args['tax_query'] = $tax_query;
}
// Filter by sku.
if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $request['sku'];
}
$args['meta_query'] = $this->add_meta_query( $args, array(
'key' => '_sku',
'value' => $skus,
'compare' => 'IN',
) );
}
// Apply all WP_Query filters again.
if ( is_array( $request['filter'] ) ) {
$args = array_merge( $args, $request['filter'] );
unset( $args['filter'] );
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = $this->post_type;
}
return $args;
}
/**
* Get the downloads for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
* @return array
*/
protected function get_downloads( $product ) {
$downloads = array();
if ( $product->is_downloadable() ) {
foreach ( $product->get_downloads() as $file_id => $file ) {
$downloads[] = array(
'id' => $file_id, // MD5 hash.
'name' => $file['name'],
'file' => $file['file'],
);
}
}
return $downloads;
}
/**
* Get taxonomy terms.
*
* @param WC_Product $product Product instance.
* @param string $taxonomy Taxonomy slug.
* @return array
*/
protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) {
$terms = array();
foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) {
$terms[] = array(
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
);
}
return $terms;
}
/**
* Get the images for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
* @return array
*/
protected function get_images( $product ) {
$images = array();
$attachment_ids = array();
// Add featured image.
if ( $product->get_image_id() ) {
$attachment_ids[] = $product->get_image_id();
}
// Add gallery images.
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
// Build image data.
foreach ( $attachment_ids as $position => $attachment_id ) {
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) ) {
continue;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
continue;
}
$images[] = array(
'id' => (int) $attachment_id,
'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ),
'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ),
'src' => current( $attachment ),
'name' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
'position' => (int) $position,
);
}
// Set a placeholder image if the product has no images set.
if ( empty( $images ) ) {
$images[] = array(
'id' => 0,
'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now.
'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ),
'src' => wc_placeholder_img_src(),
'name' => __( 'Placeholder', 'woocommerce' ),
'alt' => __( 'Placeholder', 'woocommerce' ),
'position' => 0,
);
}
return $images;
}
/**
* Get attribute taxonomy label.
*
* @param string $name Taxonomy name.
* @return string
*/
protected function get_attribute_taxonomy_label( $name ) {
$tax = get_taxonomy( $name );
$labels = get_taxonomy_labels( $tax );
return $labels->singular_name;
}
/**
* Get default attributes.
*
* @param WC_Product $product Product instance.
* @return array
*/
protected function get_default_attributes( $product ) {
$default = array();
if ( $product->is_type( 'variable' ) ) {
foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) {
if ( 0 === strpos( $key, 'pa_' ) ) {
$default[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $key ),
'name' => $this->get_attribute_taxonomy_label( $key ),
'option' => $value,
);
} else {
$default[] = array(
'id' => 0,
'name' => wc_attribute_taxonomy_slug( $key ),
'option' => $value,
);
}
}
}
return $default;
}
/**
* Get attribute options.
*
* @param int $product_id Product ID.
* @param array $attribute Attribute data.
* @return array
*/
protected function get_attribute_options( $product_id, $attribute ) {
if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) );
} elseif ( isset( $attribute['value'] ) ) {
return array_map( 'trim', explode( '|', $attribute['value'] ) );
}
return array();
}
/**
* Get the attributes for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
* @return array
*/
protected function get_attributes( $product ) {
$attributes = array();
if ( $product->is_type( 'variation' ) ) {
// Variation attributes.
foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
$name = str_replace( 'attribute_', '', $attribute_name );
if ( ! $attribute ) {
continue;
}
// Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`.
if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) {
$option_term = get_term_by( 'slug', $attribute, $name );
$attributes[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $name ),
'name' => $this->get_attribute_taxonomy_label( $name ),
'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
);
} else {
$attributes[] = array(
'id' => 0,
'name' => $name,
'option' => $attribute,
);
}
}
} else {
foreach ( $product->get_attributes() as $attribute ) {
if ( $attribute['is_taxonomy'] ) {
$attributes[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ),
'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ),
'position' => (int) $attribute['position'],
'visible' => (bool) $attribute['is_visible'],
'variation' => (bool) $attribute['is_variation'],
'options' => $this->get_attribute_options( $product->get_id(), $attribute ),
);
} else {
$attributes[] = array(
'id' => 0,
'name' => $attribute['name'],
'position' => (int) $attribute['position'],
'visible' => (bool) $attribute['is_visible'],
'variation' => (bool) $attribute['is_variation'],
'options' => $this->get_attribute_options( $product->get_id(), $attribute ),
);
}
}
}
return $attributes;
}
/**
* Get product menu order.
*
* @deprecated 3.0.0
* @param WC_Product $product Product instance.
* @return int
*/
protected function get_product_menu_order( $product ) {
return $product->get_menu_order();
}
/**
* Get product data.
*
* @param WC_Product $product Product instance.
* @return array
*/
protected function get_product_data( $product ) {
$data = array(
'id' => $product->get_id(),
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'permalink' => $product->get_permalink(),
'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ),
'type' => $product->get_type(),
'status' => $product->get_status(),
'featured' => $product->is_featured(),
'catalog_visibility' => $product->get_catalog_visibility(),
'description' => wpautop( do_shortcode( $product->get_description() ) ),
'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ),
'sku' => $product->get_sku(),
'price' => $product->get_price(),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '',
'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '',
'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '',
'price_html' => $product->get_price_html(),
'on_sale' => $product->is_on_sale(),
'purchasable' => $product->is_purchasable(),
'total_sales' => $product->get_total_sales(),
'virtual' => $product->is_virtual(),
'downloadable' => $product->is_downloadable(),
'downloads' => $this->get_downloads( $product ),
'download_limit' => $product->get_download_limit(),
'download_expiry' => $product->get_download_expiry(),
'download_type' => 'standard',
'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '',
'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '',
'tax_status' => $product->get_tax_status(),
'tax_class' => $product->get_tax_class(),
'manage_stock' => $product->managing_stock(),
'stock_quantity' => $product->get_stock_quantity(),
'in_stock' => $product->is_in_stock(),
'backorders' => $product->get_backorders(),
'backorders_allowed' => $product->backorders_allowed(),
'backordered' => $product->is_on_backorder(),
'sold_individually' => $product->is_sold_individually(),
'weight' => $product->get_weight(),
'dimensions' => array(
'length' => $product->get_length(),
'width' => $product->get_width(),
'height' => $product->get_height(),
),
'shipping_required' => $product->needs_shipping(),
'shipping_taxable' => $product->is_shipping_taxable(),
'shipping_class' => $product->get_shipping_class(),
'shipping_class_id' => $product->get_shipping_class_id(),
'reviews_allowed' => $product->get_reviews_allowed(),
'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ),
'rating_count' => $product->get_rating_count(),
'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ),
'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ),
'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ),
'parent_id' => $product->get_parent_id(),
'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ),
'categories' => $this->get_taxonomy_terms( $product ),
'tags' => $this->get_taxonomy_terms( $product, 'tag' ),
'images' => $this->get_images( $product ),
'attributes' => $this->get_attributes( $product ),
'default_attributes' => $this->get_default_attributes( $product ),
'variations' => array(),
'grouped_products' => array(),
'menu_order' => $product->get_menu_order(),
);
return $data;
}
/**
* Get an individual variation's data.
*
* @param WC_Product $product Product instance.
* @return array
*/
protected function get_variation_data( $product ) {
$variations = array();
foreach ( $product->get_children() as $child_id ) {
$variation = wc_get_product( $child_id );
if ( ! $variation || ! $variation->exists() ) {
continue;
}
$variations[] = array(
'id' => $variation->get_id(),
'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ),
'permalink' => $variation->get_permalink(),
'sku' => $variation->get_sku(),
'price' => $variation->get_price(),
'regular_price' => $variation->get_regular_price(),
'sale_price' => $variation->get_sale_price(),
'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '',
'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '',
'on_sale' => $variation->is_on_sale(),
'purchasable' => $variation->is_purchasable(),
'visible' => $variation->is_visible(),
'virtual' => $variation->is_virtual(),
'downloadable' => $variation->is_downloadable(),
'downloads' => $this->get_downloads( $variation ),
'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1,
'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1,
'tax_status' => $variation->get_tax_status(),
'tax_class' => $variation->get_tax_class(),
'manage_stock' => $variation->managing_stock(),
'stock_quantity' => $variation->get_stock_quantity(),
'in_stock' => $variation->is_in_stock(),
'backorders' => $variation->get_backorders(),
'backorders_allowed' => $variation->backorders_allowed(),
'backordered' => $variation->is_on_backorder(),
'weight' => $variation->get_weight(),
'dimensions' => array(
'length' => $variation->get_length(),
'width' => $variation->get_width(),
'height' => $variation->get_height(),
),
'shipping_class' => $variation->get_shipping_class(),
'shipping_class_id' => $variation->get_shipping_class_id(),
'image' => $this->get_images( $variation ),
'attributes' => $this->get_attributes( $variation ),
);
}
return $variations;
}
/**
* Prepare a single product output for response.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $post, $request ) {
$product = wc_get_product( $post );
$data = $this->get_product_data( $product );
// Add variations to variable products.
if ( $product->is_type( 'variable' ) && $product->has_child() ) {
$data['variations'] = $this->get_variation_data( $product );
}
// Add grouped products data.
if ( $product->is_type( 'grouped' ) && $product->has_child() ) {
$data['grouped_products'] = $product->get_children();
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $product, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Product $product Product object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given product.
*/
protected function prepare_links( $product, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( $product->get_parent_id() ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ),
);
}
return $links;
}
/**
* Prepare a single product for create or update.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|stdClass $data Post object.
*/
protected function prepare_item_for_database( $request ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $request['type'] ) ) {
$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );
if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}
$product = new $classname( $id );
} elseif ( isset( $request['id'] ) ) {
$product = wc_get_product( $id );
} else {
$product = new WC_Product_Simple();
}
// Post title.
if ( isset( $request['name'] ) ) {
$product->set_name( wp_filter_post_kses( $request['name'] ) );
}
// Post content.
if ( isset( $request['description'] ) ) {
$product->set_description( wp_filter_post_kses( $request['description'] ) );
}
// Post excerpt.
if ( isset( $request['short_description'] ) ) {
$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
}
// Post status.
if ( isset( $request['status'] ) ) {
$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
}
// Post slug.
if ( isset( $request['slug'] ) ) {
$product->set_slug( $request['slug'] );
}
// Menu order.
if ( isset( $request['menu_order'] ) ) {
$product->set_menu_order( $request['menu_order'] );
}
// Comment status.
if ( isset( $request['reviews_allowed'] ) ) {
$product->set_reviews_allowed( $request['reviews_allowed'] );
}
/**
* Filter the query_vars used in `get_items` for the constructed query.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for insertion.
*
* @param WC_Product $product An object representing a single item prepared
* for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request );
}
/**
* Create a single product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$product_id = 0;
try {
$product_id = $this->save_product( $request );
$post = get_post( $product_id );
$this->update_additional_fields_for_object( $post, $request );
$this->update_post_meta_fields( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_product', $post, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );
return $response;
} catch ( WC_Data_Exception $e ) {
$this->delete_post( $product_id );
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
$this->delete_post( $product_id );
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Update a single product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$post_id = (int) $request['id'];
if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
try {
$product_id = $this->save_product( $request );
$post = get_post( $product_id );
$this->update_additional_fields_for_object( $post, $request );
$this->update_post_meta_fields( $post, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_product', $post, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Saves a product to the database.
*
* @param WP_REST_Request $request Full details about the request.
* @return int
*/
public function save_product( $request ) {
$product = $this->prepare_item_for_database( $request );
return $product->save();
}
/**
* Save product images.
*
* @deprecated 3.0.0
* @param int $product_id
* @param array $images
* @throws WC_REST_Exception
*/
protected function save_product_images( $product_id, $images ) {
$product = wc_get_product( $product_id );
return set_product_images( $product, $images );
}
/**
* Set product images.
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param array $images Images data.
* @return WC_Product
*/
protected function set_product_images( $product, $images ) {
if ( is_array( $images ) ) {
$gallery = array();
foreach ( $images as $image ) {
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
if ( 0 === $attachment_id && isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
} else {
continue;
}
}
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {
throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
}
if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) {
$product->set_image_id( $attachment_id );
} else {
$gallery[] = $attachment_id;
}
// Set the image alt if present.
if ( ! empty( $image['alt'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
}
// Set the image name if present.
if ( ! empty( $image['name'] ) ) {
wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) );
}
}
if ( ! empty( $gallery ) ) {
$product->set_gallery_image_ids( $gallery );
}
} else {
$product->set_image_id( '' );
$product->set_gallery_image_ids( array() );
}
return $product;
}
/**
* Save product shipping data.
*
* @param WC_Product $product Product instance.
* @param array $data Shipping data.
* @return WC_Product
*/
protected function save_product_shipping_data( $product, $data ) {
// Virtual.
if ( isset( $data['virtual'] ) && true === $data['virtual'] ) {
$product->set_weight( '' );
$product->set_height( '' );
$product->set_length( '' );
$product->set_width( '' );
} else {
if ( isset( $data['weight'] ) ) {
$product->set_weight( $data['weight'] );
}
// Height.
if ( isset( $data['dimensions']['height'] ) ) {
$product->set_height( $data['dimensions']['height'] );
}
// Width.
if ( isset( $data['dimensions']['width'] ) ) {
$product->set_width( $data['dimensions']['width'] );
}
// Length.
if ( isset( $data['dimensions']['length'] ) ) {
$product->set_length( $data['dimensions']['length'] );
}
}
// Shipping class.
if ( isset( $data['shipping_class'] ) ) {
$data_store = $product->get_data_store();
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
$product->set_shipping_class_id( $shipping_class_id );
}
return $product;
}
/**
* Save downloadable files.
*
* @param WC_Product $product Product instance.
* @param array $downloads Downloads data.
* @param int $deprecated Deprecated since 3.0.
* @return WC_Product
*/
protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
if ( $deprecated ) {
wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' );
}
$files = array();
foreach ( $downloads as $key => $file ) {
if ( empty( $file['file'] ) ) {
continue;
}
$download = new WC_Product_Download();
$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
$files[] = $download;
}
$product->set_downloads( $files );
return $product;
}
/**
* Save taxonomy terms.
*
* @param WC_Product $product Product instance.
* @param array $terms Terms data.
* @param string $taxonomy Taxonomy name.
* @return WC_Product
*/
protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) {
$term_ids = wp_list_pluck( $terms, 'id' );
if ( 'cat' === $taxonomy ) {
$product->set_category_ids( $term_ids );
} elseif ( 'tag' === $taxonomy ) {
$product->set_tag_ids( $term_ids );
}
return $product;
}
/**
* Save default attributes.
*
* @since 3.0.0
*
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
* @return WC_Product
*/
protected function save_default_attributes( $product, $request ) {
if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
$attributes = $product->get_attributes();
$default_attributes = array();
foreach ( $request['default_attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( isset( $attributes[ $attribute_name ] ) ) {
$_attribute = $attributes[ $attribute_name ];
if ( $_attribute['is_variation'] ) {
$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( ! empty( $_attribute['is_taxonomy'] ) ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $value, $attribute_name );
if ( $term && ! is_wp_error( $term ) ) {
$value = $term->slug;
} else {
$value = sanitize_title( $value );
}
}
if ( $value ) {
$default_attributes[ $attribute_name ] = $value;
}
}
}
}
$product->set_default_attributes( $default_attributes );
}
return $product;
}
/**
* Save product meta.
*
* @deprecated 3.0.0
* @param WC_Product $product
* @param WP_REST_Request $request
* @return bool
* @throws WC_REST_Exception
*/
protected function save_product_meta( $product, $request ) {
$product = $this->set_product_meta( $product, $request );
$product->save();
return true;
}
/**
* Set product meta.
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
* @return WC_Product
*/
protected function set_product_meta( $product, $request ) {
// Virtual.
if ( isset( $request['virtual'] ) ) {
$product->set_virtual( $request['virtual'] );
}
// Tax status.
if ( isset( $request['tax_status'] ) ) {
$product->set_tax_status( $request['tax_status'] );
}
// Tax Class.
if ( isset( $request['tax_class'] ) ) {
$product->set_tax_class( $request['tax_class'] );
}
// Catalog Visibility.
if ( isset( $request['catalog_visibility'] ) ) {
$product->set_catalog_visibility( $request['catalog_visibility'] );
}
// Purchase Note.
if ( isset( $request['purchase_note'] ) ) {
$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
}
// Featured Product.
if ( isset( $request['featured'] ) ) {
$product->set_featured( $request['featured'] );
}
// Shipping data.
$product = $this->save_product_shipping_data( $product, $request );
// SKU.
if ( isset( $request['sku'] ) ) {
$product->set_sku( wc_clean( $request['sku'] ) );
}
// Attributes.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = wc_clean( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( $attribute_id ) {
if ( isset( $attribute['options'] ) ) {
$options = $attribute['options'];
if ( ! is_array( $attribute['options'] ) ) {
// Text based attributes - Posted values are term names.
$options = explode( WC_DELIMITER, $options );
}
$values = array_map( 'wc_sanitize_term_text_based', $options );
$values = array_filter( $values, 'strlen' );
} else {
$values = array();
}
if ( ! empty( $values ) ) {
// Add attribute to array, but don't set values.
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_id( $attribute_id );
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
} elseif ( isset( $attribute['options'] ) ) {
// Custom attribute - Add attribute to array and set the values.
if ( is_array( $attribute['options'] ) ) {
$values = $attribute['options'];
} else {
$values = explode( WC_DELIMITER, $attribute['options'] );
}
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
}
$product->set_attributes( $attributes );
}
// Sales and prices.
if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
$product->set_regular_price( '' );
$product->set_sale_price( '' );
$product->set_date_on_sale_to( '' );
$product->set_date_on_sale_from( '' );
$product->set_price( '' );
} else {
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$product->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$product->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to'] );
}
}
// Product parent ID for groups.
if ( isset( $request['parent_id'] ) ) {
$product->set_parent_id( $request['parent_id'] );
}
// Sold individually.
if ( isset( $request['sold_individually'] ) ) {
$product->set_sold_individually( $request['sold_individually'] );
}
// Stock status.
if ( isset( $request['in_stock'] ) ) {
$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
} else {
$stock_status = $product->get_stock_status();
}
// Stock data.
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
// Manage stock.
if ( isset( $request['manage_stock'] ) ) {
$product->set_manage_stock( $request['manage_stock'] );
}
// Backorders.
if ( isset( $request['backorders'] ) ) {
$product->set_backorders( $request['backorders'] );
}
if ( $product->is_type( 'grouped' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
} elseif ( $product->is_type( 'external' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( 'instock' );
} elseif ( $product->get_manage_stock() ) {
// Stock status is always determined by children so sync later.
if ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Stock quantity.
if ( isset( $request['stock_quantity'] ) ) {
$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $product->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
}
} else {
// Don't manage stock.
$product->set_manage_stock( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
}
} elseif ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Upsells.
if ( isset( $request['upsell_ids'] ) ) {
$upsells = array();
$ids = $request['upsell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$upsells[] = $id;
}
}
}
$product->set_upsell_ids( $upsells );
}
// Cross sells.
if ( isset( $request['cross_sell_ids'] ) ) {
$crosssells = array();
$ids = $request['cross_sell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$crosssells[] = $id;
}
}
}
$product->set_cross_sell_ids( $crosssells );
}
// Product categories.
if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['categories'] );
}
// Product tags.
if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
}
// Downloadable.
if ( isset( $request['downloadable'] ) ) {
$product->set_downloadable( $request['downloadable'] );
}
// Downloadable options.
if ( $product->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$product = $this->save_downloadable_files( $product, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$product->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$product->set_download_expiry( $request['download_expiry'] );
}
}
// Product url and button text for external products.
if ( $product->is_type( 'external' ) ) {
if ( isset( $request['external_url'] ) ) {
$product->set_product_url( $request['external_url'] );
}
if ( isset( $request['button_text'] ) ) {
$product->set_button_text( $request['button_text'] );
}
}
// Save default attributes for variable products.
if ( $product->is_type( 'variable' ) ) {
$product = $this->save_default_attributes( $product, $request );
}
return $product;
}
/**
* Save variations.
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
* @return bool
*/
protected function save_variations_data( $product, $request ) {
foreach ( $request['variations'] as $menu_order => $data ) {
$variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 );
// Create initial name and status.
if ( ! $variation->get_slug() ) {
/* translators: 1: variation id 2: product name */
$variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) );
$variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' );
}
// Parent ID.
$variation->set_parent_id( $product->get_id() );
// Menu order.
$variation->set_menu_order( $menu_order );
// Status.
if ( isset( $data['visible'] ) ) {
$variation->set_status( false === $data['visible'] ? 'private' : 'publish' );
}
// SKU.
if ( isset( $data['sku'] ) ) {
$variation->set_sku( wc_clean( $data['sku'] ) );
}
// Thumbnail.
if ( isset( $data['image'] ) && is_array( $data['image'] ) ) {
$image = $data['image'];
$image = current( $image );
if ( is_array( $image ) ) {
$image['position'] = 0;
}
$variation = $this->set_product_images( $variation, array( $image ) );
}
// Virtual variation.
if ( isset( $data['virtual'] ) ) {
$variation->set_virtual( $data['virtual'] );
}
// Downloadable variation.
if ( isset( $data['downloadable'] ) ) {
$variation->set_downloadable( $data['downloadable'] );
}
// Downloads.
if ( $variation->get_downloadable() ) {
// Downloadable files.
if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) {
$variation = $this->save_downloadable_files( $variation, $data['downloads'] );
}
// Download limit.
if ( isset( $data['download_limit'] ) ) {
$variation->set_download_limit( $data['download_limit'] );
}
// Download expiry.
if ( isset( $data['download_expiry'] ) ) {
$variation->set_download_expiry( $data['download_expiry'] );
}
}
// Shipping data.
$variation = $this->save_product_shipping_data( $variation, $data );
// Stock handling.
if ( isset( $data['manage_stock'] ) ) {
$variation->set_manage_stock( $data['manage_stock'] );
}
if ( isset( $data['in_stock'] ) ) {
$variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' );
}
if ( isset( $data['backorders'] ) ) {
$variation->set_backorders( $data['backorders'] );
}
if ( $variation->get_manage_stock() ) {
if ( isset( $data['stock_quantity'] ) ) {
$variation->set_stock_quantity( $data['stock_quantity'] );
} elseif ( isset( $data['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $data['inventory_delta'] );
$variation->set_stock_quantity( $stock_quantity );
}
} else {
$variation->set_backorders( 'no' );
$variation->set_stock_quantity( '' );
}
// Regular Price.
if ( isset( $data['regular_price'] ) ) {
$variation->set_regular_price( $data['regular_price'] );
}
// Sale Price.
if ( isset( $data['sale_price'] ) ) {
$variation->set_sale_price( $data['sale_price'] );
}
if ( isset( $data['date_on_sale_from'] ) ) {
$variation->set_date_on_sale_from( $data['date_on_sale_from'] );
}
if ( isset( $data['date_on_sale_to'] ) ) {
$variation->set_date_on_sale_to( $data['date_on_sale_to'] );
}
// Tax class.
if ( isset( $data['tax_class'] ) ) {
$variation->set_tax_class( $data['tax_class'] );
}
// Description.
if ( isset( $data['description'] ) ) {
$variation->set_description( wp_kses_post( $data['description'] ) );
}
// Update taxonomies.
if ( isset( $data['attributes'] ) ) {
$attributes = array();
$parent_attributes = $product->get_attributes();
foreach ( $data['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
continue;
}
$attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $attribute_value, $attribute_name );
if ( $term && ! is_wp_error( $term ) ) {
$attribute_value = $term->slug;
} else {
$attribute_value = sanitize_title( $attribute_value );
}
}
$attributes[ $attribute_key ] = $attribute_value;
}
$variation->set_attributes( $attributes );
}
$variation->save();
do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data );
}
return true;
}
/**
* Add post meta fields.
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request data.
* @return bool|WP_Error
*/
protected function add_post_meta_fields( $post, $request ) {
return $this->update_post_meta_fields( $post, $request );
}
/**
* Update post meta fields.
*
* @param WP_Post $post Post data.
* @param WP_REST_Request $request Request data.
* @return bool|WP_Error
*/
protected function update_post_meta_fields( $post, $request ) {
$product = wc_get_product( $post );
// Check for featured/gallery images, upload it and set it.
if ( isset( $request['images'] ) ) {
$product = $this->set_product_images( $product, $request['images'] );
}
// Save product meta fields.
$product = $this->set_product_meta( $product, $request );
// Save the product data.
$product->save();
// Save variations.
if ( $product->is_type( 'variable' ) ) {
if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) {
$this->save_variations_data( $product, $request );
}
}
// Clear caches here so in sync with any new variations/children.
wc_delete_product_transients( $product->get_id() );
wp_cache_delete( 'product-' . $product->get_id(), 'products' );
return true;
}
/**
* Clear cache/transients.
*
* @param WP_Post $post Post data.
*/
public function clear_transients( $post ) {
wc_delete_product_transients( $post->ID );
}
/**
* Delete post.
*
* @param int|WP_Post $id Post ID or WP_Post instance.
*/
protected function delete_post( $id ) {
if ( ! empty( $id->ID ) ) {
$id = $id->ID;
} elseif ( ! is_numeric( $id ) || 0 >= $id ) {
return;
}
// Delete product attachments.
$attachments = get_posts( array(
'post_parent' => $id,
'post_status' => 'any',
'post_type' => 'attachment',
) );
foreach ( (array) $attachments as $attachment ) {
wp_delete_attachment( $attachment->ID, true );
}
// Delete product.
$product = wc_get_product( $id );
$product->delete( true );
}
/**
* Delete a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$force = (bool) $request['force'];
$post = get_post( $id );
$product = wc_get_product( $id );
if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$supports_trash = EMPTY_TRASH_DAYS > 0;
/**
* Filter whether an item is trashable.
*
* Return false to disable trash support for the item.
*
* @param boolean $supports_trash Whether the item type support trashing.
* @param WP_Post $post The Post object being considered for trashing support.
*/
$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
// If we're forcing, then delete permanently.
if ( $force ) {
if ( $product->is_type( 'variable' ) ) {
foreach ( $product->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! empty( $child ) ) {
$child->delete( true );
}
}
} else {
// For other product types, if the product has children, remove the relationship.
foreach ( $product->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! empty( $child ) ) {
$child->set_parent_id( 0 );
$child->save();
}
}
}
$product->delete( true );
$result = ! ( $product->get_id() > 0 );
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
}
// Otherwise, only trash if we haven't already.
if ( 'trash' === $post->post_status ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
}
// (Note that internally this falls through to `wp_delete_post` if
// the trash is disabled.)
$product->delete();
$result = 'trash' === $product->get_status();
}
if ( ! $result ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
}
// Delete parent product transients.
if ( $parent_id = wp_get_post_parent_id( $id ) ) {
wc_delete_product_transients( $parent_id );
}
/**
* Fires after a single item is deleted or trashed via the REST API.
*
* @param object $post The deleted or trashed item.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request );
return $response;
}
/**
* Get the Product's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) );
$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'slug' => array(
'description' => __( 'Product slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Product type.', 'woocommerce' ),
'type' => 'string',
'default' => 'simple',
'enum' => array_keys( wc_get_product_types() ),
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Product status (post status).', 'woocommerce' ),
'type' => 'string',
'default' => 'publish',
'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
'context' => array( 'view', 'edit' ),
),
'featured' => array(
'description' => __( 'Featured product.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'catalog_visibility' => array(
'description' => __( 'Catalog visibility.', 'woocommerce' ),
'type' => 'string',
'default' => 'visible',
'enum' => array( 'visible', 'catalog', 'search', 'hidden' ),
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Product description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'short_description' => array(
'description' => __( 'Product short description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Product regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Product sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( 'Start date of sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( 'End date of sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price_html' => array(
'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'on_sale' => array(
'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'purchasable' => array(
'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_sales' => array(
'description' => __( 'Amount of sales.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'virtual' => array(
'description' => __( 'If the product is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the product is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_type' => array(
'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ),
'type' => 'string',
'default' => 'standard',
'enum' => array( 'standard' ),
'context' => array( 'view', 'edit' ),
),
'external_url' => array(
'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'button_text' => array(
'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at product level.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(
'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sold_individually' => array(
'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Product dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_required' => array(
'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_taxable' => array(
'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'reviews_allowed' => array(
'description' => __( 'Allow reviews.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'average_rating' => array(
'description' => __( 'Reviews average rating.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rating_count' => array(
'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'related_ids' => array(
'description' => __( 'List of related products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'upsell_ids' => array(
'description' => __( 'List of upsell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'cross_sell_ids' => array(
'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'parent_id' => array(
'description' => __( 'Product parent ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'purchase_note' => array(
'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'categories' => array(
'description' => __( 'List of categories.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Category ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Category slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'tags' => array(
'description' => __( 'List of tags.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tag ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Tag name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Tag slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'images' => array(
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
),
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Attribute position.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'visible' => array(
'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'variation' => array(
'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'options' => array(
'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
),
),
),
),
'default_attributes' => array(
'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'variations' => array(
'description' => __( 'List of variations.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'permalink' => array(
'description' => __( 'Variation URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current variation price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Variation regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Variation sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( 'Start date of sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( 'End date of sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'on_sale' => array(
'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'purchasable' => array(
'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'visible' => array(
'description' => __( 'If the variation is visible.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
),
'virtual' => array(
'description' => __( 'If the variation is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => null,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => null,
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at variation level.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(
'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Variation dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'image' => array(
'description' => __( 'Variation image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
),
'grouped_products' => array(
'description' => __( 'List of grouped products ID.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['slug'] = array(
'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['status'] = array(
'default' => 'any',
'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_merge( array( 'any', 'future' ), array_keys( get_post_statuses() ) ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['type'] = array(
'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_types() ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['category'] = array(
'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tag'] = array(
'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['shipping_class'] = array(
'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute'] = array(
'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_term'] = array(
'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['sku'] = array(
'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-report-sales-v1-controller.php 0000644 00000030713 15154275221 0021113 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/sales endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Report Sales controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Report_Sales_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/sales';
/**
* Report instance.
*
* @var WC_Admin_Report
*/
protected $report;
/**
* Register the routes for sales reports.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read report.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get sales reports.
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
$item = $this->prepare_item_for_response( null, $request );
$data[] = $this->prepare_response_for_collection( $item );
return rest_ensure_response( $data );
}
/**
* Prepare a report sales object for serialization.
*
* @param null $_
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $_, $request ) {
// Set date filtering.
$filter = array(
'period' => $request['period'],
'date_min' => $request['date_min'],
'date_max' => $request['date_max'],
);
$this->setup_report( $filter );
// New customers.
$users_query = new WP_User_Query(
array(
'fields' => array( 'user_registered' ),
'role' => 'customer',
)
);
$customers = $users_query->get_results();
foreach ( $customers as $key => $customer ) {
if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
unset( $customers[ $key ] );
}
}
$total_customers = count( $customers );
$report_data = $this->report->get_report_data();
$period_totals = array();
// Setup period totals by ensuring each period in the interval has data.
for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) {
switch ( $this->report->chart_groupby ) {
case 'day' :
$time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
break;
default :
$time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
break;
}
// Set the customer signups for each period.
$customer_count = 0;
foreach ( $customers as $customer ) {
if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
$customer_count++;
}
}
$period_totals[ $time ] = array(
'sales' => wc_format_decimal( 0.00, 2 ),
'orders' => 0,
'items' => 0,
'tax' => wc_format_decimal( 0.00, 2 ),
'shipping' => wc_format_decimal( 0.00, 2 ),
'discount' => wc_format_decimal( 0.00, 2 ),
'customers' => $customer_count,
);
}
// add total sales, total order count, total tax and total shipping for each period
foreach ( $report_data->orders as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
$period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
$period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
}
foreach ( $report_data->order_counts as $order ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['orders'] = (int) $order->count;
}
// Add total order items for each period.
foreach ( $report_data->order_items as $order_item ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
}
// Add total discount for each period.
foreach ( $report_data->coupons as $discount ) {
$time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
if ( ! isset( $period_totals[ $time ] ) ) {
continue;
}
$period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
}
$sales_data = array(
'total_sales' => $report_data->total_sales,
'net_sales' => $report_data->net_sales,
'average_sales' => $report_data->average_sales,
'total_orders' => $report_data->total_orders,
'total_items' => $report_data->total_items,
'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
'total_shipping' => $report_data->total_shipping,
'total_refunds' => $report_data->total_refunds,
'total_discount' => $report_data->total_coupons,
'totals_grouped_by' => $this->report->chart_groupby,
'totals' => $period_totals,
'total_customers' => $total_customers,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $sales_data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( array(
'about' => array(
'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ),
),
) );
/**
* Filter a report sales returned from the API.
*
* Allows modification of the report sales data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $data The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request );
}
/**
* Setup the report object and parse any date filtering.
*
* @param array $filter date filtering
*/
protected function setup_report( $filter ) {
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
$this->report = new WC_Report_Sales_By_Date();
if ( empty( $filter['period'] ) ) {
// Custom date range.
$filter['period'] = 'custom';
if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
// Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges.
$_GET['start_date'] = $filter['date_min'];
$_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null;
} else {
// Default custom range to today.
$_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
}
} else {
$filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period'];
// Change "week" period to "7day".
if ( 'week' === $filter['period'] ) {
$filter['period'] = '7day';
}
}
$this->report->calculate_current_range( $filter['period'] );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'sales_report',
'type' => 'object',
'properties' => array(
'total_sales' => array(
'description' => __( 'Gross sales in the period.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'net_sales' => array(
'description' => __( 'Net sales in the period.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'average_sales' => array(
'description' => __( 'Average net daily sales.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total_orders' => array(
'description' => __( 'Total of orders placed.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'total_items' => array(
'description' => __( 'Total of items purchased.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Total charged for taxes.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total_shipping' => array(
'description' => __( 'Total charged for shipping.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total_refunds' => array(
'description' => __( 'Total of refunded orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'total_discount' => array(
'description' => __( 'Total of coupons used.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'totals_grouped_by' => array(
'description' => __( 'Group type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'totals' => array(
'description' => __( 'Totals.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'array',
),
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
'period' => array(
'description' => __( 'Report period.', 'woocommerce' ),
'type' => 'string',
'enum' => array( 'week', 'month', 'last_month', 'year' ),
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'sanitize_text_field',
),
'date_min' => array(
/* translators: %s: date format */
'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
'type' => 'string',
'format' => 'date',
'validate_callback' => 'wc_rest_validate_reports_request_arg',
'sanitize_callback' => 'sanitize_text_field',
),
'date_max' => array(
/* translators: %s: date format */
'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
'type' => 'string',
'format' => 'date',
'validate_callback' => 'wc_rest_validate_reports_request_arg',
'sanitize_callback' => 'sanitize_text_field',
),
);
}
}
Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php 0000644 00000011153 15154275222 0022253 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/top_sellers endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Report Top Sellers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Report_Sales_V1_Controller
*/
class WC_REST_Report_Top_Sellers_V1_Controller extends WC_REST_Report_Sales_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/top_sellers';
/**
* Get sales reports.
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_items( $request ) {
// Set date filtering.
$filter = array(
'period' => $request['period'],
'date_min' => $request['date_min'],
'date_max' => $request['date_max'],
);
$this->setup_report( $filter );
$report_data = $this->report->get_order_report_data( array(
'data' => array(
'_product_id' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => '',
'name' => 'product_id',
),
'_qty' => array(
'type' => 'order_item_meta',
'order_item_type' => 'line_item',
'function' => 'SUM',
'name' => 'order_item_qty',
),
),
'order_by' => 'order_item_qty DESC',
'group_by' => 'product_id',
'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12,
'query_type' => 'get_results',
'filter_range' => true,
) );
$top_sellers = array();
foreach ( $report_data as $item ) {
$product = wc_get_product( $item->product_id );
if ( $product ) {
$top_sellers[] = array(
'name' => $product->get_name(),
'product_id' => (int) $item->product_id,
'quantity' => wc_stock_amount( $item->order_item_qty ),
);
}
}
$data = array();
foreach ( $top_sellers as $top_seller ) {
$item = $this->prepare_item_for_response( (object) $top_seller, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Prepare a report sales object for serialization.
*
* @param stdClass $top_seller
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $top_seller, $request ) {
$data = array(
'name' => $top_seller->name,
'product_id' => $top_seller->product_id,
'quantity' => $top_seller->quantity,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( array(
'about' => array(
'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ),
),
'product' => array(
'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ),
),
) );
/**
* Filter a report top sellers returned from the API.
*
* Allows modification of the report top sellers data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $top_seller The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'top_sellers_report',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'quantity' => array(
'description' => __( 'Total number of purchases.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version1/class-wc-rest-reports-v1-controller.php 0000644 00000011251 15154275223 0020167 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Reports controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Reports_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports';
/**
* Register the routes for reports.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read reports.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
return array(
array(
'slug' => 'sales',
'description' => __( 'List of sales reports.', 'woocommerce' ),
),
array(
'slug' => 'top_sellers',
'description' => __( 'List of top sellers products.', 'woocommerce' ),
),
);
}
/**
* Get all reports.
*
* @param WP_REST_Request $request
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
$reports = $this->get_reports();
foreach ( $reports as $report ) {
$item = $this->prepare_item_for_response( (object) $report, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'description' => $report->description,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php 0000644 00000023010 15154275223 0020714 0 ustar 00 <?php
/**
* REST API Tax Classes controller
*
* Handles requests to the /taxes/classes endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Tax Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Tax_Classes_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'taxes/classes';
/**
* Register the routes for tax classes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)',
array(
'args' => array(
'slug' => array(
'description' => __( 'Unique slug for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to read tax classes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access create tax classes.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access delete a tax.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all tax classes.
*
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
public function get_items( $request ) {
$tax_classes = array();
// Add standard class.
$tax_classes[] = array(
'slug' => 'standard',
'name' => __( 'Standard rate', 'woocommerce' ),
);
$classes = WC_Tax::get_tax_classes();
foreach ( $classes as $class ) {
$tax_classes[] = array(
'slug' => sanitize_title( $class ),
'name' => $class,
);
}
$data = array();
foreach ( $tax_classes as $tax_class ) {
$class = $this->prepare_item_for_response( $tax_class, $request );
$class = $this->prepare_response_for_collection( $class );
$data[] = $class;
}
return rest_ensure_response( $data );
}
/**
* Create a single tax class.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
$tax_class = WC_Tax::create_tax_class( $request['name'] );
if ( is_wp_error( $tax_class ) ) {
return new WP_Error( 'woocommerce_rest_' . $tax_class->get_error_code(), $tax_class->get_error_message(), array( 'status' => 400 ) );
}
$this->update_additional_fields_for_object( $tax_class, $request );
/**
* Fires after a tax class is created or updated via the REST API.
*
* @param stdClass $tax_class Data used to create the tax class.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating tax class, false when updating tax class.
*/
do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tax_class, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) );
return $response;
}
/**
* Delete a single tax class.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
global $wpdb;
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
$deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
if ( ! $deleted ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) );
}
if ( is_wp_error( $deleted ) ) {
return new WP_Error( 'woocommerce_rest_' . $deleted->get_error_code(), $deleted->get_error_message(), array( 'status' => 400 ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tax_class, $request );
/**
* Fires after a tax class is deleted via the REST API.
*
* @param stdClass $tax_class The tax data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request );
return $response;
}
/**
* Prepare a single tax class output for response.
*
* @param array $tax_class Tax class data.
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $tax_class, $request ) {
$data = $tax_class;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links() );
/**
* Filter tax object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $tax_class Tax object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request );
}
/**
* Prepare links for the request.
*
* @return array Links for the given tax class.
*/
protected function prepare_links() {
$links = array(
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the Tax Classes schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'tax_class',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Tax class name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'required' => true,
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-taxes-v1-controller.php 0000644 00000056722 15154275224 0017632 0 ustar 00 <?php
/**
* REST API Taxes controller
*
* Handles requests to the /taxes endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Taxes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'taxes';
/**
* Register the routes for taxes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check whether a given request has permission to read taxes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access create taxes.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access update a tax.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function update_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access delete a tax.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all taxes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
global $wpdb;
$prepared_args = array();
$prepared_args['order'] = $request['order'];
$prepared_args['number'] = $request['per_page'];
if ( ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$orderby_possibles = array(
'id' => 'tax_rate_id',
'order' => 'tax_rate_order',
'priority' => 'tax_rate_priority',
);
$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
$prepared_args['class'] = $request['class'];
/**
* Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API.
*
* @param array $prepared_args Array of arguments for $wpdb->get_results().
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request );
$orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] );
$query = "
SELECT *
FROM {$wpdb->prefix}woocommerce_tax_rates
%s
ORDER BY {$orderby}
LIMIT %%d, %%d
";
$wpdb_prepare_args = array(
$prepared_args['offset'],
$prepared_args['number'],
);
// Filter by tax class.
if ( empty( $prepared_args['class'] ) ) {
$query = sprintf( $query, '' );
} else {
$class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : '';
array_unshift( $wpdb_prepare_args, $class );
$query = sprintf( $query, 'WHERE tax_rate_class = %s' );
}
// Query taxes.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results(
$wpdb->prepare(
$query,
$wpdb_prepare_args
)
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
$taxes = array();
foreach ( $results as $tax ) {
$data = $this->prepare_item_for_response( $tax, $request );
$taxes[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $taxes );
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
// Unset LIMIT args.
array_splice( $wpdb_prepare_args, -2 );
// Count query.
$query = str_replace(
array(
'SELECT *',
'LIMIT %d, %d',
),
array(
'SELECT COUNT(*)',
'',
),
$query
);
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$total_taxes = (int) $wpdb->get_var( empty( $wpdb_prepare_args ) ? $query : $wpdb->prepare( $query, $wpdb_prepare_args ) );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
// Calculate totals.
$response->header( 'X-WP-Total', $total_taxes );
$max_pages = ceil( $total_taxes / $per_page );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Take tax data from the request and return the updated or newly created rate.
*
* @param WP_REST_Request $request Full details about the request.
* @param stdClass|null $current Existing tax object.
* @return object
*/
protected function create_or_update_tax( $request, $current = null ) {
$id = absint( isset( $request['id'] ) ? $request['id'] : 0 );
$data = array();
$fields = array(
'tax_rate_country',
'tax_rate_state',
'tax_rate',
'tax_rate_name',
'tax_rate_priority',
'tax_rate_compound',
'tax_rate_shipping',
'tax_rate_order',
'tax_rate_class',
);
foreach ( $fields as $field ) {
// Keys via API differ from the stored names returned by _get_tax_rate.
$key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field );
// Remove data that was not posted.
if ( ! isset( $request[ $key ] ) ) {
continue;
}
// Test new data against current data.
if ( $current && $current->$field === $request[ $key ] ) {
continue;
}
// Add to data array.
switch ( $key ) {
case 'tax_rate_priority':
case 'tax_rate_compound':
case 'tax_rate_shipping':
case 'tax_rate_order':
$data[ $field ] = absint( $request[ $key ] );
break;
case 'tax_rate_class':
$data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : '';
break;
default:
$data[ $field ] = wc_clean( $request[ $key ] );
break;
}
}
if ( ! $id ) {
$id = WC_Tax::_insert_tax_rate( $data );
} elseif ( $data ) {
WC_Tax::_update_tax_rate( $id, $data );
}
// Add locales.
if ( ! empty( $request['postcode'] ) ) {
WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) );
}
if ( ! empty( $request['city'] ) ) {
WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) );
}
return WC_Tax::_get_tax_rate( $id, OBJECT );
}
/**
* Create a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) );
}
$tax = $this->create_or_update_tax( $request );
$this->update_additional_fields_for_object( $tax, $request );
/**
* Fires after a tax is created or updated via the REST API.
*
* @param stdClass $tax Data used to create the tax.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating tax, false when updating tax.
*/
do_action( 'woocommerce_rest_insert_tax', $tax, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tax, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) );
return $response;
}
/**
* Get a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT );
if ( empty( $id ) || empty( $tax_obj ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$tax = $this->prepare_item_for_response( $tax_obj, $request );
$response = rest_ensure_response( $tax );
return $response;
}
/**
* Update a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$id = (int) $request['id'];
$tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT );
if ( empty( $id ) || empty( $tax_obj ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$tax = $this->create_or_update_tax( $request, $tax_obj );
$this->update_additional_fields_for_object( $tax, $request );
/**
* Fires after a tax is created or updated via the REST API.
*
* @param stdClass $tax Data used to create the tax.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating tax, false when updating tax.
*/
do_action( 'woocommerce_rest_insert_tax', $tax, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tax, $request );
$response = rest_ensure_response( $response );
return $response;
}
/**
* Delete a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
global $wpdb;
$id = (int) $request['id'];
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$tax = WC_Tax::_get_tax_rate( $id, OBJECT );
if ( empty( $id ) || empty( $tax ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tax, $request );
WC_Tax::_delete_tax_rate( $id );
if ( 0 === $wpdb->rows_affected ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
/**
* Fires after a tax is deleted via the REST API.
*
* @param stdClass $tax The tax data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request );
return $response;
}
/**
* Prepare a single tax output for response.
*
* @param stdClass $tax Tax object.
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $tax, $request ) {
$id = (int) $tax->tax_rate_id;
$data = array(
'id' => $id,
'country' => $tax->tax_rate_country,
'state' => $tax->tax_rate_state,
'postcode' => '',
'city' => '',
'rate' => $tax->tax_rate,
'name' => $tax->tax_rate_name,
'priority' => (int) $tax->tax_rate_priority,
'compound' => (bool) $tax->tax_rate_compound,
'shipping' => (bool) $tax->tax_rate_shipping,
'order' => (int) $tax->tax_rate_order,
'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard',
);
$data = $this->add_tax_rate_locales( $data, $tax );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $tax ) );
/**
* Filter tax object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $tax Tax object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request );
}
/**
* Prepare links for the request.
*
* @param stdClass $tax Tax object.
* @return array Links for the given tax.
*/
protected function prepare_links( $tax ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Add tax rate locales to the response array.
*
* @param array $data Response data.
* @param stdClass $tax Tax object.
*
* @return array
*/
protected function add_tax_rate_locales( $data, $tax ) {
global $wpdb;
// Get locales from a tax rate.
$locales = $wpdb->get_results(
$wpdb->prepare(
"
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
",
$tax->tax_rate_id
)
);
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
$data[ $locale->location_type ] = $locale->location_code;
}
}
return $data;
}
/**
* Get the Taxes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'tax',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'country' => array(
'description' => __( 'Country ISO 3166 code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'State code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postcode / ZIP.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'rate' => array(
'description' => __( 'Tax rate.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Tax rate name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'priority' => array(
'description' => __( 'Tax priority.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'context' => array( 'view', 'edit' ),
),
'compound' => array(
'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'shipping' => array(
'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'order' => array(
'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'default' => 'standard',
'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param();
$params['context']['default'] = 'view';
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'default' => 'asc',
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'enum' => array( 'asc', 'desc' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'default' => 'order',
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'enum' => array(
'id',
'order',
'priority',
),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['class'] = array(
'description' => __( 'Sort by tax class.', 'woocommerce' ),
'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
'sanitize_callback' => 'sanitize_title',
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php 0000644 00000023020 15154275225 0022257 0 ustar 00 <?php
/**
* REST API Webhooks controller
*
* Handles requests to the /webhooks/<webhook_id>/deliveries endpoint.
*
* @author WooThemes
* @category API
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Webhook Deliveries controller class.
*
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Webhook_Deliveries_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'webhooks/(?P<webhook_id>[\d]+)/deliveries';
/**
* Register the routes for webhook deliveries.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
'args' => array(
'webhook_id' => array(
'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'webhook_id' => array(
'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read taxes.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all webhook deliveries.
*
* @param WP_REST_Request $request
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$webhook = wc_get_webhook( (int) $request['webhook_id'] );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$logs = array();
$data = array();
foreach ( $logs as $log ) {
$delivery = $this->prepare_item_for_response( (object) $log, $request );
$delivery = $this->prepare_response_for_collection( $delivery );
$data[] = $delivery;
}
return rest_ensure_response( $data );
}
/**
* Get a single webhook delivery.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$webhook = wc_get_webhook( (int) $request['webhook_id'] );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$log = array();
if ( empty( $id ) || empty( $log ) ) {
return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$delivery = $this->prepare_item_for_response( (object) $log, $request );
$response = rest_ensure_response( $delivery );
return $response;
}
/**
* Prepare a single webhook delivery output for response.
*
* @param stdClass $log Delivery log object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $log, $request ) {
$data = (array) $log;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $log ) );
/**
* Filter webhook delivery object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $log Delivery log object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request );
}
/**
* Prepare links for the request.
*
* @param stdClass $log Delivery log object.
* @return array Links for the given webhook delivery.
*/
protected function prepare_links( $log ) {
$webhook_id = (int) $log->request_headers['X-WC-Webhook-ID'];
$base = str_replace( '(?P<webhook_id>[\d]+)', $webhook_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ),
),
);
return $links;
}
/**
* Get the Webhook's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'webhook_delivery',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'duration' => array(
'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'summary' => array(
'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'request_url' => array(
'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
'request_headers' => array(
'description' => __( 'Request headers.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'request_body' => array(
'description' => __( 'Request body.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_code' => array(
'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_message' => array(
'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_headers' => array(
'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'response_body' => array(
'description' => __( 'The response body from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version1/class-wc-rest-webhooks-v1-controller.php 0000644 00000063646 15154275226 0020334 0 ustar 00 <?php
/**
* REST API Webhooks controller
*
* Handles requests to the /webhooks endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Webhooks controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Webhooks_V1_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'webhooks';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_webhook';
/**
* Register the routes for webhooks.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
array(
'topic' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Webhook topic.', 'woocommerce' ),
),
'delivery_url' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Webhook delivery URL.', 'woocommerce' ),
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check whether a given request has permission to read webhooks.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access create webhooks.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a webhook.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access update a webhook.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function update_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access delete a webhook.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'delete' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'webhooks', 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get the default REST API version.
*
* @since 3.0.0
* @return string
*/
protected function get_default_api_version() {
return 'wp_api_v1';
}
/**
* Get all webhooks.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$args = array();
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['status'] = 'all' === $request['status'] ? '' : $request['status'];
$args['include'] = implode( ',', $request['include'] );
$args['exclude'] = implode( ',', $request['exclude'] );
$args['limit'] = $request['per_page'];
$args['search'] = $request['search'];
$args['before'] = $request['before'];
$args['after'] = $request['after'];
if ( empty( $request['offset'] ) ) {
$args['offset'] = 1 < $request['page'] ? ( $request['page'] - 1 ) * $args['limit'] : 0;
}
/**
* Filter arguments, before passing to WC_Webhook_Data_Store->search_webhooks, when querying webhooks via the REST API.
*
* @param array $args Array of arguments for $wpdb->get_results().
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( 'woocommerce_rest_webhook_query', $args, $request );
unset( $prepared_args['page'] );
$prepared_args['paginate'] = true;
// Get the webhooks.
$webhooks = array();
$data_store = WC_Data_Store::load( 'webhook' );
$results = $data_store->search_webhooks( $prepared_args );
$webhook_ids = $results->webhooks;
foreach ( $webhook_ids as $webhook_id ) {
$data = $this->prepare_item_for_response( $webhook_id, $request );
$webhooks[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $webhooks );
$per_page = (int) $prepared_args['limit'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$total_webhooks = $results->total;
$max_pages = $results->max_num_pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
$response->header( 'X-WP-Total', $total_webhooks );
$response->header( 'X-WP-TotalPages', $max_pages );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Get a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
if ( empty( $id ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $id, $request );
$response = rest_ensure_response( $data );
return $response;
}
/**
* Create a single webhook.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
// Validate topic.
if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
}
// Validate delivery URL.
if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) );
}
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
$webhook = new WC_Webhook();
$webhook->set_name( $post->post_title );
$webhook->set_user_id( $post->post_author );
$webhook->set_status( 'publish' === $post->post_status ? 'active' : 'disabled' );
$webhook->set_topic( $request['topic'] );
$webhook->set_delivery_url( $request['delivery_url'] );
$webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : wp_generate_password( 50, true, true ) );
$webhook->set_api_version( $this->get_default_api_version() );
$webhook->save();
$this->update_additional_fields_for_object( $webhook, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WC_Webhook $webhook Webhook data.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_webhook_object', $webhook, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $webhook->get_id(), $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $webhook->get_id() ) ) );
// Send ping.
$webhook->deliver_ping();
return $response;
}
/**
* Update a single webhook.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$id = (int) $request['id'];
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
// Update topic.
if ( ! empty( $request['topic'] ) ) {
if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) {
$webhook->set_topic( $request['topic'] );
} else {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
// Update delivery URL.
if ( ! empty( $request['delivery_url'] ) ) {
if ( wc_is_valid_url( $request['delivery_url'] ) ) {
$webhook->set_delivery_url( $request['delivery_url'] );
} else {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
// Update secret.
if ( ! empty( $request['secret'] ) ) {
$webhook->set_secret( $request['secret'] );
}
// Update status.
if ( ! empty( $request['status'] ) ) {
if ( wc_is_webhook_valid_status( strtolower( $request['status'] ) ) ) {
$webhook->set_status( $request['status'] );
} else {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_status", __( 'Webhook status must be valid.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
if ( isset( $post->post_title ) ) {
$webhook->set_name( $post->post_title );
}
$webhook->save();
$this->update_additional_fields_for_object( $webhook, $request );
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WC_Webhook $webhook Webhook data.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_webhook_object', $webhook, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $webhook->get_id(), $request );
return rest_ensure_response( $response );
}
/**
* Delete a single webhook.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $webhook, $request );
$result = $webhook->delete( true );
if ( ! $result ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
}
/**
* Fires after a single item is deleted or trashed via the REST API.
*
* @param WC_Webhook $webhook The deleted or trashed item.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_webhook_object', $webhook, $response, $request );
return $response;
}
/**
* Prepare a single webhook for create or update.
*
* @param WP_REST_Request $request Request object.
* @return WP_Error|stdClass $data Post object.
*/
protected function prepare_item_for_database( $request ) {
$data = new stdClass();
// Post ID.
if ( isset( $request['id'] ) ) {
$data->ID = absint( $request['id'] );
}
// Validate required POST fields.
if ( 'POST' === $request->get_method() && empty( $data->ID ) ) {
$data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), (new DateTime('now'))->format( _x( 'M d, Y @ h:i A', 'Webhook created on date parsed by DateTime::format', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine
// Post author.
$data->post_author = get_current_user_id();
// Post password.
$data->post_password = 'webhook_' . wp_generate_password();
// Post status.
$data->post_status = 'publish';
} else {
// Allow edit post title.
if ( ! empty( $request['name'] ) ) {
$data->post_title = $request['name'];
}
}
// Comment status.
$data->comment_status = 'closed';
// Ping status.
$data->ping_status = 'closed';
/**
* Filter the query_vars used in `get_items` for the constructed query.
*
* The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being
* prepared for insertion.
*
* @param stdClass $data An object representing a single item prepared
* for inserting or updating the database.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request );
}
/**
* Prepare a single webhook output for response.
*
* @param int $id Webhook ID or object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $id, $request ) {
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
$data = array(
'id' => $webhook->get_id(),
'name' => $webhook->get_name(),
'status' => $webhook->get_status(),
'topic' => $webhook->get_topic(),
'resource' => $webhook->get_resource(),
'event' => $webhook->get_event(),
'hooks' => $webhook->get_hooks(),
'delivery_url' => $webhook->get_delivery_url(),
'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified() ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $webhook->get_id() ) );
/**
* Filter webhook object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WC_Webhook $webhook Webhook object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request );
}
/**
* Prepare links for the request.
*
* @param int $id Webhook ID.
* @return array
*/
protected function prepare_links( $id ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the Webhook's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'webhook',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'A friendly name for the webhook.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Webhook status.', 'woocommerce' ),
'type' => 'string',
'default' => 'active',
'enum' => array_keys( wc_get_webhook_statuses() ),
'context' => array( 'view', 'edit' ),
),
'topic' => array(
'description' => __( 'Webhook topic.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'resource' => array(
'description' => __( 'Webhook resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'event' => array(
'description' => __( 'Webhook event.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hooks' => array(
'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'delivery_url' => array(
'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'secret' => array(
'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'date_created' => array(
'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['context']['default'] = 'view';
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'id',
'title',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['status'] = array(
'default' => 'all',
'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ),
'type' => 'string',
'enum' => array( 'all', 'active', 'paused', 'disabled' ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version2/class-wc-rest-coupons-v2-controller.php 0000644 00000046043 15154275230 0020166 0 ustar 00 <?php
/**
* REST API Coupons controller
*
* Handles requests to the /coupons endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API Coupons controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Coupons_V2_Controller extends WC_REST_CRUD_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'coupons';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_coupon';
/**
* Register the routes for coupons.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'required' => true,
'type' => 'string',
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Get object.
*
* @since 3.0.0
* @param int $id Object ID.
* @return WC_Data
*/
protected function get_object( $id ) {
return new WC_Coupon( $id );
}
/**
* Get formatted item data.
*
* @since 3.0.0
* @param WC_Data $object WC_Data instance.
* @return array
*/
protected function get_formatted_item_data( $object ) {
$data = $object->get_data();
$format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' );
$format_date = array( 'date_created', 'date_modified', 'date_expires' );
$format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], 2 );
}
// Format date values.
foreach ( $format_date as $key ) {
$datetime = $data[ $key ];
$data[ $key ] = wc_rest_prepare_date_response( $datetime, false );
$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
}
// Format null values.
foreach ( $format_null as $key ) {
$data[ $key ] = $data[ $key ] ? $data[ $key ] : null;
}
return array(
'id' => $object->get_id(),
'code' => $data['code'],
'amount' => $data['amount'],
'status' => $data['status'],
'date_created' => $data['date_created'],
'date_created_gmt' => $data['date_created_gmt'],
'date_modified' => $data['date_modified'],
'date_modified_gmt' => $data['date_modified_gmt'],
'discount_type' => $data['discount_type'],
'description' => $data['description'],
'date_expires' => $data['date_expires'],
'date_expires_gmt' => $data['date_expires_gmt'],
'usage_count' => $data['usage_count'],
'individual_use' => $data['individual_use'],
'product_ids' => $data['product_ids'],
'excluded_product_ids' => $data['excluded_product_ids'],
'usage_limit' => $data['usage_limit'],
'usage_limit_per_user' => $data['usage_limit_per_user'],
'limit_usage_to_x_items' => $data['limit_usage_to_x_items'],
'free_shipping' => $data['free_shipping'],
'product_categories' => $data['product_categories'],
'excluded_product_categories' => $data['excluded_product_categories'],
'exclude_sale_items' => $data['exclude_sale_items'],
'minimum_amount' => $data['minimum_amount'],
'maximum_amount' => $data['maximum_amount'],
'email_restrictions' => $data['email_restrictions'],
'used_by' => $data['used_by'],
'meta_data' => $data['meta_data'],
);
}
/**
* Prepare a single coupon output for response.
*
* @since 3.0.0
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$data = $this->get_formatted_item_data( $object );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
$coupon_code = $request['code'] ?? null;
if ( ! StringUtil::is_null_or_whitespace( $coupon_code ) ) {
$id = wc_get_coupon_id_by_code( $coupon_code );
$args['post__in'] = array( $id );
}
// Get only ids.
$args['fields'] = 'ids';
return $args;
}
/**
* Only return writable props from schema.
*
* @param array $schema Schema.
* @return bool
*/
protected function filter_writable_props( $schema ) {
return empty( $schema['readonly'] );
}
/**
* Prepare a single coupon for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$coupon = new WC_Coupon( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Validate required POST fields.
if ( $creating && StringUtil::is_null_or_whitespace( $request['code'] ?? null ) ) {
return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) );
}
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'code':
$coupon_code = wc_format_coupon_code( $value );
$id = $coupon->get_id() ? $coupon->get_id() : 0;
$id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id );
if ( $id_from_code ) {
return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) );
}
$coupon->set_code( $coupon_code );
break;
case 'meta_data':
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
case 'description':
$coupon->set_description( wp_filter_post_kses( $value ) );
break;
default:
if ( is_callable( array( $coupon, "set_{$key}" ) ) ) {
$coupon->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $coupon Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating );
}
/**
* Get the Coupon's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the object.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'amount' => array(
'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'The status of the coupon. Should always be draft, published, or pending review', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'discount_type' => array(
'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ),
'type' => 'string',
'default' => 'fixed_cart',
'enum' => array_keys( wc_get_coupon_types() ),
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Coupon description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_expires' => array(
'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_expires_gmt' => array(
'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'usage_count' => array(
'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'individual_use' => array(
'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'product_ids' => array(
'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'excluded_product_ids' => array(
'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'usage_limit' => array(
'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'usage_limit_per_user' => array(
'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'limit_usage_to_x_items' => array(
'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'free_shipping' => array(
'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'product_categories' => array(
'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'excluded_product_categories' => array(
'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'exclude_sale_items' => array(
'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'minimum_amount' => array(
'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'maximum_amount' => array(
'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email_restrictions' => array(
'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
),
'used_by' => array(
'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['code'] = array(
'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php 0000644 00000012675 15154275231 0022336 0 ustar 00 <?php
/**
* REST API Customer Downloads controller
*
* Handles requests to the /customers/<customer_id>/downloads endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Customer_Downloads_V1_Controller
*/
class WC_REST_Customer_Downloads_V2_Controller extends WC_REST_Customer_Downloads_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Prepare a single download output for response.
*
* @param stdClass $download Download object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $download, $request ) {
$data = array(
'download_id' => $download->download_id,
'download_url' => $download->download_url,
'product_id' => $download->product_id,
'product_name' => $download->product_name,
'download_name' => $download->download_name,
'order_id' => $download->order_id,
'order_key' => $download->order_key,
'downloads_remaining' => '' === $download->downloads_remaining ? 'unlimited' : $download->downloads_remaining,
'access_expires' => $download->access_expires ? wc_rest_prepare_date_response( $download->access_expires ) : 'never',
'access_expires_gmt' => $download->access_expires ? wc_rest_prepare_date_response( get_gmt_from_date( $download->access_expires ) ) : 'never',
'file' => $download->file,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $download, $request ) );
/**
* Filter customer download data returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $download Download object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request );
}
/**
* Get the Customer Download's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'customer_download',
'type' => 'object',
'properties' => array(
'download_id' => array(
'description' => __( 'Download ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'download_url' => array(
'description' => __( 'Download file URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Downloadable product ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'product_name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'download_name' => array(
'description' => __( 'Downloadable file name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'order_id' => array(
'description' => __( 'Order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'order_key' => array(
'description' => __( 'Order key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'downloads_remaining' => array(
'description' => __( 'Number of downloads remaining.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'access_expires' => array(
'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'access_expires_gmt' => array(
'description' => __( 'The date when download access expires, as GMT.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'file' => array(
'description' => __( 'File details.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-customers-v2-controller.php 0000644 00000032426 15154275231 0020525 0 ustar 00 <?php
/**
* REST API Customers controller
*
* Handles requests to the /customers endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Customers_V1_Controller
*/
class WC_REST_Customers_V2_Controller extends WC_REST_Customers_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Get formatted item data.
*
* @since 3.0.0
* @param WC_Data $object WC_Data instance.
* @return array
*/
protected function get_formatted_item_data( $object ) {
$formatted_data = $this->get_formatted_item_data_core( $object );
$formatted_data['orders_count'] = $object->get_order_count();
$formatted_data['total_spent'] = $object->get_total_spent();
return $formatted_data;
}
/**
* Get formatted item data, not including orders count nor total spent.
* This method is needed because v3 API doesn't return those two fields.
*
* @internal This method could disappear or have its name or signature changed in future releases.
*
* @param WC_Data $object WC_Data instance.
* @return array
*/
protected function get_formatted_item_data_core( $object ) {
$data = $object->get_data();
$format_date = array( 'date_created', 'date_modified' );
// Format date values.
foreach ( $format_date as $key ) {
// Date created is stored UTC, date modified is stored WP local time.
$datetime = 'date_created' === $key && is_subclass_of( $data[ $key ], 'DateTime' ) ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ];
$data[ $key ] = wc_rest_prepare_date_response( $datetime, false );
$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
}
$formatted_data = array(
'id' => $object->get_id(),
'date_created' => $data['date_created'],
'date_created_gmt' => $data['date_created_gmt'],
'date_modified' => $data['date_modified'],
'date_modified_gmt' => $data['date_modified_gmt'],
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'role' => $data['role'],
'username' => $data['username'],
'billing' => $data['billing'],
'shipping' => $data['shipping'],
'is_paying_customer' => $data['is_paying_customer'],
'avatar_url' => $object->get_avatar_url(),
);
if ( wc_current_user_has_role( 'administrator' ) ) {
$formatted_data['meta_data'] = $data['meta_data'];
}
return $formatted_data;
}
/**
* Prepare a single customer output for response.
*
* @param WP_User $user_data User object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $user_data, $request ) {
$customer = new WC_Customer( $user_data->ID );
$data = $this->get_formatted_item_data( $customer );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $user_data ) );
//phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
/**
* Filter customer data returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_User $user_data User object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request );
//phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
}
/**
* Update customer meta fields.
*
* @param WC_Customer $customer Customer data.
* @param WP_REST_Request $request Request data.
*/
protected function update_customer_meta_fields( $customer, $request ) {
parent::update_customer_meta_fields( $customer, $request );
// Meta data.
if ( isset( $request['meta_data'] ) ) {
if ( is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$customer->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
}
}
/**
* Get the Customer's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'customer',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'email' => array(
'description' => __( 'The email address for the customer.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'first_name' => array(
'description' => __( 'Customer first name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'last_name' => array(
'description' => __( 'Customer last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'role' => array(
'description' => __( 'Customer role.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'username' => array(
'description' => __( 'Customer login name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_user',
),
),
'password' => array(
'description' => __( 'Customer password.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'billing' => array(
'description' => __( 'List of billing address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping' => array(
'description' => __( 'List of shipping address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'is_paying_customer' => array(
'description' => __( 'Is the customer a paying customer?', 'woocommerce' ),
'type' => 'bool',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_spent' => array(
'description' => __( 'Total amount spent.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avatar_url' => array(
'description' => __( 'Avatar URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-network-orders-v2-controller.php 0000644 00000012222 15154275231 0021456 0 ustar 00 <?php
/**
* REST API Network Orders controller
*
* Handles requests to the /orders/network endpoint
*
* @package WooCommerce\RestApi
* @since 3.4.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Network Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Orders_V2_Controller
*/
class WC_REST_Network_Orders_V2_Controller extends WC_REST_Orders_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Register the routes for network orders.
*/
public function register_routes() {
if ( is_multisite() ) {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/network',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'network_orders' ),
'permission_callback' => array( $this, 'network_orders_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
}
/**
* Retrieves the item's schema for display / public consumption purposes.
*
* @return array Public item schema data.
*/
public function get_public_item_schema() {
$schema = parent::get_public_item_schema();
$schema['properties']['blog'] = array(
'description' => __( 'Blog id of the record on the multisite.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
);
$schema['properties']['edit_url'] = array(
'description' => __( 'URL to edit the order', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
);
$schema['properties']['customer'][] = array(
'description' => __( 'Name of the customer for the order', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
);
$schema['properties']['status_name'][] = array(
'description' => __( 'Order Status', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
);
$schema['properties']['formatted_total'][] = array(
'description' => __( 'Order total formatted for locale', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
);
return $schema;
}
/**
* Does a permissions check for the proper requested blog
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool $permission
*/
public function network_orders_permissions_check( $request ) {
$blog_id = $request->get_param( 'blog_id' );
$blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id();
switch_to_blog( $blog_id );
$permission = $this->get_items_permissions_check( $request );
restore_current_blog();
return $permission;
}
/**
* Get a collection of orders from the requested blog id
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response
*/
public function network_orders( $request ) {
$blog_id = $request->get_param( 'blog_id' );
$blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id();
$active_plugins = get_blog_option( $blog_id, 'active_plugins', array() );
$network_active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
$plugins = array_merge( $active_plugins, $network_active_plugins );
$wc_active = false;
foreach ( $plugins as $plugin ) {
if ( substr_compare( $plugin, '/woocommerce.php', strlen( $plugin ) - strlen( '/woocommerce.php' ), strlen( '/woocommerce.php' ) ) === 0 ) {
$wc_active = true;
}
}
// If WooCommerce not active for site, return an empty response.
if ( ! $wc_active ) {
$response = rest_ensure_response( array() );
return $response;
}
switch_to_blog( $blog_id );
add_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) );
$items = $this->get_items( $request );
remove_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) );
foreach ( $items->data as &$current_order ) {
$order = wc_get_order( $current_order['id'] );
$current_order['blog'] = get_blog_details( get_current_blog_id() );
$current_order['edit_url'] = get_admin_url( $blog_id, 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' );
/* translators: 1: first name 2: last name */
$current_order['customer'] = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) );
$current_order['status_name'] = wc_get_order_status_name( $order->get_status() );
$current_order['formatted_total'] = $order->get_formatted_order_total();
}
restore_current_blog();
return $items;
}
/**
* Filters the post statuses to on hold and processing for the network order query.
*
* @param array $args Query args.
*
* @return array
*/
public function network_orders_filter_args( $args ) {
$args['post_status'] = array(
'wc-on-hold',
'wc-processing',
);
return $args;
}
}
Controllers/Version2/class-wc-rest-order-notes-v2-controller.php 0000644 00000012764 15154275233 0020747 0 ustar 00 <?php
/**
* REST API Order Notes controller
*
* Handles requests to the /orders/<order_id>/notes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Order Notes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Order_Notes_V1_Controller
*/
class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Get order notes from an order.
*
* @param WP_REST_Request $request Request data.
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$args = array(
'post_id' => $order->get_id(),
'approve' => 'approve',
'type' => 'order_note',
);
// Allow filter by order note type.
if ( 'customer' === $request['type'] ) {
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => 'is_customer_note',
'value' => 1,
'compare' => '=',
),
);
} elseif ( 'internal' === $request['type'] ) {
$args['meta_query'] = array( // WPCS: slow query ok.
array(
'key' => 'is_customer_note',
'compare' => 'NOT EXISTS',
),
);
}
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$notes = get_comments( $args );
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
$data = array();
foreach ( $notes as $note ) {
$order_note = $this->prepare_item_for_response( $note, $request );
$order_note = $this->prepare_response_for_collection( $order_note );
$data[] = $order_note;
}
return rest_ensure_response( $data );
}
/**
* Prepare a single order note output for response.
*
* @param WP_Comment $note Order note object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $note, $request ) {
$data = array(
'id' => (int) $note->comment_ID,
'date_created' => wc_rest_prepare_date_response( $note->comment_date ),
'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ),
'note' => $note->comment_content,
'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $note ) );
/**
* Filter order note object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $note Order note object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
}
/**
* Get the Order Notes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order_note',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'note' => array(
'description' => __( 'Order note content.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'customer_note' => array(
'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['type'] = array(
'default' => 'any',
'description' => __( 'Limit result to customers or internal notes.', 'woocommerce' ),
'type' => 'string',
'enum' => array( 'any', 'customer', 'internal' ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php 0000644 00000045405 15154275233 0021263 0 ustar 00 <?php
/**
* REST API Order Refunds controller
*
* Handles requests to the /orders/<order_id>/refunds endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Order Refunds controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Orders_V2_Controller
*/
class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'orders/(?P<order_id>[\d]+)/refunds';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_order_refund';
/**
* Stores the request.
*
* @var array
*/
protected $request = array();
/**
* Order refunds actions.
*/
public function __construct() {
add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' );
}
/**
* Register the routes for order refunds.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => true,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get object.
*
* @since 3.0.0
* @param int $id Object ID.
* @return WC_Data
*/
protected function get_object( $id ) {
return wc_get_order( $id );
}
/**
* Get formatted item data.
*
* @since 3.0.0
* @param WC_Data $object WC_Data instance.
* @return array
*/
protected function get_formatted_item_data( $object ) {
$data = $object->get_data();
$format_decimal = array( 'amount' );
$format_date = array( 'date_created' );
$format_line_items = array( 'line_items', 'shipping_lines', 'tax_lines', 'fee_lines' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
}
// Format date values.
foreach ( $format_date as $key ) {
$datetime = $data[ $key ];
$data[ $key ] = wc_rest_prepare_date_response( $datetime, false );
$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
}
// Format line items.
foreach ( $format_line_items as $key ) {
$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
}
return array(
'id' => $object->get_id(),
'date_created' => $data['date_created'],
'date_created_gmt' => $data['date_created_gmt'],
'amount' => $data['amount'],
'reason' => $data['reason'],
'refunded_by' => $data['refunded_by'],
'refunded_payment' => $data['refunded_payment'],
'meta_data' => $data['meta_data'],
'line_items' => $data['line_items'],
'shipping_lines' => $data['shipping_lines'],
'tax_lines' => $data['tax_lines'],
'fee_lines' => $data['fee_lines'],
);
}
/**
* Prepare a single order output for response.
*
* @since 3.0.0
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*
* @return WP_Error|WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$this->request = $request;
$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order ) {
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
}
if ( ! $object || $object->get_parent_id() !== $order->get_id() ) {
return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 );
}
$data = $this->get_formatted_item_data( $object );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {
$base = str_replace( '(?P<order_id>[\d]+)', $object->get_parent_id(), $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ),
),
);
return $links;
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
$args['post_status'] = array_keys( wc_get_order_statuses() );
$args['post_parent__in'] = array( absint( $request['order_id'] ) );
return $args;
}
/**
* Prepares one object for create or update operation.
*
* @since 3.0.0
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order ) {
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
}
if ( 0 > $request['amount'] ) {
return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
}
// Create the refund.
$refund = wc_create_refund(
array(
'order_id' => $order->get_id(),
'amount' => $request['amount'],
'reason' => empty( $request['reason'] ) ? null : $request['reason'],
'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true,
'restock_items' => true,
)
);
if ( is_wp_error( $refund ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
}
if ( ! $refund ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
}
if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
$refund->save_meta_data();
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $coupon Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
}
/**
* Save an object data.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @param bool $creating If is creating a new object.
* @return WC_Data|WP_Error
*/
protected function save_object( $request, $creating = false ) {
try {
$object = $this->prepare_object_for_database( $request, $creating );
if ( is_wp_error( $object ) ) {
return $object;
}
return $this->get_object( $object->get_id() );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Get the refund schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the order refund was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'amount' => array(
'description' => __( 'Refund amount.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'reason' => array(
'description' => __( 'Reason for refund.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'refunded_by' => array(
'description' => __( 'User ID of user who created the refund.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'refunded_payment' => array(
'description' => __( 'If the payment was refunded via the API.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
'line_items' => array(
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'variation_id' => array(
'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'quantity' => array(
'description' => __( 'Quantity ordered.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tax_class' => array(
'description' => __( 'Tax class of product.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal_tax' => array(
'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'sku' => array(
'description' => __( 'Product SKU.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'price' => array(
'description' => __( 'Product price.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'api_refund' => array(
'description' => __( 'When true, the payment gateway API is used to generate the refund.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'default' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
unset( $params['status'], $params['customer'], $params['product'] );
return $params;
}
}
Controllers/Version2/class-wc-rest-orders-v2-controller.php 0000644 00000212262 15154275233 0017777 0 ustar 00 <?php
/**
* REST API Orders controller
*
* Handles requests to the /orders endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
// phpcs:disable Squiz.Classes.ClassFileName.NoMatch, Squiz.Classes.ValidClassName.NotCamelCaps -- Legacy class name, can't change without breaking backward compat.
/**
* REST API Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
// phpcs:enable
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'orders';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'shop_order';
/**
* If object is hierarchical.
*
* @var bool
*/
protected $hierarchical = true;
/**
* Stores the request.
*
* @var array
*/
protected $request = array();
/**
* Register the routes for orders.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Get object. Return false if object is not of required type.
*
* @since 3.0.0
* @param int $id Object ID.
* @return WC_Data|bool
*/
protected function get_object( $id ) {
$order = wc_get_order( $id );
// In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path.
if ( ! $order || 'shop_order_refund' === $order->get_type() ) {
return false;
}
return $order;
}
/**
* Check if a given request has access to read an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::get_item_permissions_check( $request );
}
/**
* Check if a given request has access to update an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::update_item_permissions_check( $request );
}
/**
* Check if a given request has access to delete an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( ( ! $object || 0 === $object->get_id() ) && ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return parent::delete_item_permissions_check( $request );
}
/**
* Expands an order item to get its data.
*
* @param WC_Order_item $item Order item data.
* @return array
*/
protected function get_order_item_data( $item ) {
$data = $item->get_data();
$format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
if ( isset( $data[ $key ] ) ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
}
}
// Add SKU, PRICE, and IMAGE to products.
if ( is_callable( array( $item, 'get_product' ) ) ) {
$data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null;
$data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0;
$image_id = $item->get_product() ? $item->get_product()->get_image_id() : 0;
$data['image'] = array(
'id' => $image_id,
'src' => $image_id ? wp_get_attachment_image_url( $image_id, 'full' ) : '',
);
}
// Add parent_name if the product is a variation.
if ( is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
if ( is_callable( array( $product, 'get_parent_data' ) ) ) {
$data['parent_name'] = $product->get_title();
} else {
$data['parent_name'] = null;
}
}
// Format taxes.
if ( ! empty( $data['taxes']['total'] ) ) {
$taxes = array();
foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) {
$taxes[] = array(
'id' => $tax_rate_id,
'total' => $tax,
'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '',
);
}
$data['taxes'] = $taxes;
} elseif ( isset( $data['taxes'] ) ) {
$data['taxes'] = array();
}
// Remove names for coupons, taxes and shipping.
if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) {
unset( $data['name'] );
}
// Remove props we don't want to expose.
unset( $data['order_id'] );
unset( $data['type'] );
// Expand meta_data to include user-friendly values.
$formatted_meta_data = $item->get_all_formatted_meta_data( null );
// Filter out product variations.
if ( isset( $product ) && 'true' === $this->request['order_item_display_meta'] ) {
$order_item_name = $data['name'];
$data['meta_data'] = array_filter(
$data['meta_data'],
function( $meta ) use ( $product, $order_item_name ) {
$display_value = wp_kses_post( rawurldecode( (string) $meta->value ) );
// Skip items with values already in the product details area of the product name.
if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) {
return false;
}
return true;
}
);
}
$data['meta_data'] = array_map(
array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ),
$data['meta_data'],
array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data )
);
return $data;
}
/**
* Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding
* {@link WC_Meta_Data}. Returns the merged array.
*
* @param WC_Meta_Data $meta_item An object from {@link WC_Order_Item::get_meta_data()}.
* @param array $formatted_meta_data An object result from {@link WC_Order_Item::get_all_formatted_meta_data}.
* The keys are the IDs of {@link WC_Meta_Data}.
*
* @return array
*/
private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) {
$result = array(
'id' => $meta_item->id,
'key' => $meta_item->key,
'value' => $meta_item->value,
'display_key' => $meta_item->key, // Default to original key, in case a formatted key is not available.
'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available.
);
if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) {
$formatted_meta_item = $formatted_meta_data[ $meta_item->id ];
$result['display_key'] = wc_clean( $formatted_meta_item->display_key );
$result['display_value'] = wc_clean( $formatted_meta_item->display_value );
}
return $result;
}
/**
* With HPOS, few internal meta keys such as _billing_address_index, _shipping_address_index are not considered internal anymore (since most internal keys were flattened into dedicated columns).
*
* This function helps in filtering out any remaining internal meta keys with HPOS is enabled.
*
* @param array $meta_data Order meta data.
*
* @return array Filtered order meta data.
*/
private function filter_internal_meta_keys( $meta_data ) {
if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
return $meta_data;
}
$cpt_hidden_keys = ( new \WC_Order_Data_Store_CPT() )->get_internal_meta_keys();
$meta_data = array_filter(
$meta_data,
function ( $meta ) use ( $cpt_hidden_keys ) {
return ! in_array( $meta->key, $cpt_hidden_keys, true );
}
);
return array_values( $meta_data );
}
/**
* Get formatted item data.
*
* @since 3.0.0
* @param WC_Order $order WC_Data instance.
*
* @return array
*/
protected function get_formatted_item_data( $order ) {
$extra_fields = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds', 'payment_url', 'is_editable', 'needs_payment', 'needs_processing' );
$format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' );
$format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' );
// These fields are dependent on other fields.
$dependent_fields = array(
'date_created_gmt' => 'date_created',
'date_modified_gmt' => 'date_modified',
'date_completed_gmt' => 'date_completed',
'date_paid_gmt' => 'date_paid',
);
$format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' );
// Only fetch fields that we need.
$fields = $this->get_fields_for_response( $this->request );
foreach ( $dependent_fields as $field_key => $dependency ) {
if ( in_array( $field_key, $fields, true ) && ! in_array( $dependency, $fields, true ) ) {
$fields[] = $dependency;
}
}
$extra_fields = array_intersect( $extra_fields, $fields );
$format_decimal = array_intersect( $format_decimal, $fields );
$format_date = array_intersect( $format_date, $fields );
$format_line_items = array_intersect( $format_line_items, $fields );
$data = $order->get_base_data();
// Add extra data as necessary.
foreach ( $extra_fields as $field ) {
switch ( $field ) {
case 'meta_data':
$meta_data = $order->get_meta_data();
$data['meta_data'] = $this->get_meta_data_for_response( $this->request, $meta_data );
$data['meta_data'] = $this->filter_internal_meta_keys( $data['meta_data'] );
break;
case 'line_items':
$data['line_items'] = $order->get_items( 'line_item' );
break;
case 'tax_lines':
$data['tax_lines'] = $order->get_items( 'tax' );
break;
case 'shipping_lines':
$data['shipping_lines'] = $order->get_items( 'shipping' );
break;
case 'fee_lines':
$data['fee_lines'] = $order->get_items( 'fee' );
break;
case 'coupon_lines':
$data['coupon_lines'] = $order->get_items( 'coupon' );
break;
case 'refunds':
$data['refunds'] = array();
foreach ( $order->get_refunds() as $refund ) {
$data['refunds'][] = array(
'id' => $refund->get_id(),
'reason' => $refund->get_reason() ? $refund->get_reason() : '',
'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ),
);
}
break;
case 'payment_url':
$data['payment_url'] = $order->get_checkout_payment_url();
break;
case 'is_editable':
$data['is_editable'] = $order->is_editable();
break;
case 'needs_payment':
$data['needs_payment'] = $order->needs_payment();
break;
case 'needs_processing':
$data['needs_processing'] = $order->needs_processing();
break;
}
}
// Format decimal values.
foreach ( $format_decimal as $key ) {
$data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] );
}
// Format date values.
foreach ( $format_date as $key ) {
$datetime = $data[ $key ];
$data[ $key ] = wc_rest_prepare_date_response( $datetime, false );
$data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime );
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
// Format line items.
foreach ( $format_line_items as $key ) {
$data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) );
}
$allowed_fields = array(
'id',
'parent_id',
'number',
'order_key',
'created_via',
'version',
'status',
'currency',
'date_created',
'date_created_gmt',
'date_modified',
'date_modified_gmt',
'discount_total',
'discount_tax',
'shipping_total',
'shipping_tax',
'cart_tax',
'total',
'total_tax',
'prices_include_tax',
'customer_id',
'customer_ip_address',
'customer_user_agent',
'customer_note',
'billing',
'shipping',
'payment_method',
'payment_method_title',
'transaction_id',
'date_paid',
'date_paid_gmt',
'date_completed',
'date_completed_gmt',
'cart_hash',
'meta_data',
'line_items',
'tax_lines',
'shipping_lines',
'fee_lines',
'coupon_lines',
'refunds',
'payment_url',
'is_editable',
'needs_payment',
'needs_processing',
);
$data = array_intersect_key( $data, array_flip( $allowed_fields ) );
return $data;
}
/**
* Prepare a single order output for response.
*
* @since 3.0.0
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$this->request = $request;
$this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] );
$request['context'] = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->get_formatted_item_data( $object );
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $request['context'] );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*
* @since 4.5.0
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( 0 !== (int) $object->get_customer_id() ) {
$links['customer'] = array(
'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ),
);
}
if ( 0 !== (int) $object->get_parent_id() ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ),
);
}
return $links;
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
global $wpdb;
$args = parent::prepare_objects_query( $request );
// Set post_status.
if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) {
$args['post_status'] = 'wc-' . $request['status'];
} elseif ( 'any' === $request['status'] ) {
$args['post_status'] = 'any';
} else {
$args['post_status'] = $request['status'];
}
if ( isset( $request['customer'] ) ) {
if ( ! empty( $args['meta_query'] ) ) {
$args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
}
$args['meta_query'][] = array(
'key' => '_customer_user',
'value' => $request['customer'],
'type' => 'NUMERIC',
);
}
// Search by product.
if ( ! empty( $request['product'] ) ) {
$order_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT order_id
FROM {$wpdb->prefix}woocommerce_order_items
WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d )
AND order_item_type = 'line_item'",
$request['product']
)
);
// Force WP_Query return empty if don't found any order.
$order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 );
$args['post__in'] = $order_ids;
}
// Search.
if ( ! OrderUtil::custom_orders_table_usage_is_enabled() && ! empty( $args['s'] ) ) {
$order_ids = wc_order_search( $args['s'] );
if ( ! empty( $order_ids ) ) {
unset( $args['s'] );
$args['post__in'] = array_merge( $order_ids, array( 0 ) );
}
}
/**
* Filter the query arguments for a request.
*
* Enables adding extra arguments or setting defaults for an order collection request.
*
* @param array $args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*
* @since 4.5.0.
*/
$args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request );
return $args;
}
/**
* Only return writable props from schema.
*
* @param array $schema Schema.
* @return bool
*/
protected function filter_writable_props( $schema ) {
return empty( $schema['readonly'] );
}
/**
* Prepare a single order for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$order = new WC_Order( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'status':
// Status change should be done later so transitions have new data.
break;
case 'billing':
case 'shipping':
$this->update_address( $order, $value, $key );
break;
case 'line_items':
case 'shipping_lines':
case 'fee_lines':
case 'coupon_lines':
if ( is_array( $value ) ) {
foreach ( $value as $item ) {
if ( is_array( $item ) ) {
if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
$order->remove_item( $item['id'] );
} else {
$this->set_item( $order, $key, $item );
}
}
}
}
break;
case 'meta_data':
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
default:
if ( is_callable( array( $order, "set_{$key}" ) ) ) {
$order->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $order Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*
* @since 4.5.0.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating );
}
/**
* Save an object data.
*
* @since 3.0.0
* @throws WC_REST_Exception But all errors are validated before returning any data.
* @param WP_REST_Request $request Full details about the request.
* @param bool $creating If is creating a new object.
* @return WC_Data|WP_Error
*/
protected function save_object( $request, $creating = false ) {
try {
$object = $this->prepare_object_for_database( $request, $creating );
if ( is_wp_error( $object ) ) {
return $object;
}
// Make sure gateways are loaded so hooks from gateways fire on save/create.
WC()->payment_gateways();
if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) {
// Make sure customer exists.
if ( false === get_user_by( 'id', $request['customer_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
}
// Make sure customer is part of blog.
if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
}
}
if ( $creating ) {
$object->set_created_via( 'rest-api' );
$object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
$object->save();
$object->calculate_totals();
} else {
// If items have changed, recalculate order totals.
if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
$object->calculate_totals( true );
}
}
// Set status.
if ( ! empty( $request['status'] ) ) {
$object->set_status( $request['status'] );
}
$object->save();
// Actions for after the order is saved.
if ( true === $request['set_paid'] ) {
if ( $creating || $object->needs_payment() ) {
$object->payment_complete( $request['transaction_id'] );
}
}
return $this->get_object( $object->get_id() );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Update address.
*
* @param WC_Order $order Order data.
* @param array $posted Posted data.
* @param string $type Type of address; 'billing' or 'shipping'.
*/
protected function update_address( $order, $posted, $type = 'billing' ) {
foreach ( $posted as $key => $value ) {
if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) {
$order->{"set_{$type}_{$key}"}( $value );
}
}
}
/**
* Gets the product ID from the SKU or posted ID.
*
* @throws WC_REST_Exception When SKU or ID is not valid.
* @param array $posted Request data.
* @param string $action 'create' to add line item or 'update' to update it.
* @return int
*/
protected function get_product_id( $posted, $action = 'create' ) {
if ( ! empty( $posted['sku'] ) ) {
$product_id = (int) wc_get_product_id_by_sku( $posted['sku'] );
} elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) {
$product_id = (int) $posted['product_id'];
} elseif ( ! empty( $posted['variation_id'] ) ) {
$product_id = (int) $posted['variation_id'];
} elseif ( 'update' === $action ) {
$product_id = 0;
} else {
throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 );
}
return $product_id;
}
/**
* Maybe set an item prop if the value was posted.
*
* @param WC_Order_Item $item Order item.
* @param string $prop Order property.
* @param array $posted Request data.
*/
protected function maybe_set_item_prop( $item, $prop, $posted ) {
if ( isset( $posted[ $prop ] ) ) {
$item->{"set_$prop"}( $posted[ $prop ] );
}
}
/**
* Maybe set item props if the values were posted.
*
* @param WC_Order_Item $item Order item data.
* @param string[] $props Properties.
* @param array $posted Request data.
*/
protected function maybe_set_item_props( $item, $props, $posted ) {
foreach ( $props as $prop ) {
$this->maybe_set_item_prop( $item, $prop, $posted );
}
}
/**
* Maybe set item meta if posted.
*
* @param WC_Order_Item $item Order item data.
* @param array $posted Request data.
*/
protected function maybe_set_item_meta_data( $item, $posted ) {
if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) {
foreach ( $posted['meta_data'] as $meta ) {
if ( isset( $meta['key'] ) ) {
$value = isset( $meta['value'] ) ? $meta['value'] : null;
$item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
}
}
/**
* Create or update a line item.
*
* @param array $posted Line item data.
* @param string $action 'create' to add line item or 'update' to update it.
* @param object $item Passed when updating an item. Null during creation.
* @return WC_Order_Item_Product
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_line_items( $posted, $action = 'create', $item = null ) {
$item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
$product = wc_get_product( $this->get_product_id( $posted, $action ) );
if ( $product && $product !== $item->get_product() ) {
$item->set_product( $product );
if ( 'create' === $action ) {
$quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1;
$total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
$item->set_total( $total );
$item->set_subtotal( $total );
}
}
$this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
return $item;
}
/**
* Create or update an order shipping method.
*
* @param array $posted $shipping Item data.
* @param string $action 'create' to add shipping or 'update' to update it.
* @param object $item Passed when updating an item. Null during creation.
* @return WC_Order_Item_Shipping
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) {
$item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
if ( 'create' === $action ) {
if ( empty( $posted['method_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
return $item;
}
/**
* Create or update an order fee.
*
* @param array $posted Item data.
* @param string $action 'create' to add fee or 'update' to update it.
* @param object $item Passed when updating an item. Null during creation.
* @return WC_Order_Item_Fee
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) {
$item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
if ( 'create' === $action ) {
if ( empty( $posted['name'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
return $item;
}
/**
* Create or update an order coupon.
*
* @param array $posted Item data.
* @param string $action 'create' to add coupon or 'update' to update it.
* @param object $item Passed when updating an item. Null during creation.
* @return WC_Order_Item_Coupon
* @throws WC_REST_Exception Invalid data, server error.
*/
protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) {
$item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item;
if ( 'create' === $action ) {
$coupon_code = ArrayUtil::get_value_or_default( $posted, 'code' );
if ( StringUtil::is_null_or_whitespace( $coupon_code ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
}
}
$this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted );
$this->maybe_set_item_meta_data( $item, $posted );
return $item;
}
/**
* Wrapper method to create/update order items.
* When updating, the item ID provided is checked to ensure it is associated
* with the order.
*
* @param WC_Order $order order object.
* @param string $item_type The item type.
* @param array $posted item provided in the request body.
* @throws WC_REST_Exception If item ID is not associated with order.
*/
protected function set_item( $order, $item_type, $posted ) {
global $wpdb;
if ( ! empty( $posted['id'] ) ) {
$action = 'update';
} else {
$action = 'create';
}
$method = 'prepare_' . $item_type;
$item = null;
// Verify provided line item ID is associated with order.
if ( 'update' === $action ) {
$item = $order->get_item( absint( $posted['id'] ), false );
if ( ! $item ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 );
}
}
// Prepare item data.
$item = $this->$method( $posted, $action, $item );
/**
* Allow extensions be notified before the item before is saved.
*
* @param WC_Order_Item $item The item object.
* @param array $posted The item data.
*
* @since 4.5.0.
*/
do_action( 'woocommerce_rest_set_order_item', $item, $posted );
// If creating the order, add the item to it.
if ( 'create' === $action ) {
$order->add_item( $item );
} else {
$item->save();
}
}
/**
* Helper method to check if the resource ID associated with the provided item is null.
* Items can be deleted by setting the resource ID to null.
*
* @param array $item Item provided in the request body.
* @return bool True if the item resource ID is null, false otherwise.
*/
protected function item_is_null( $item ) {
$keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' );
foreach ( $keys as $key ) {
if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) {
return true;
}
}
return false;
}
/**
* Get order statuses without prefixes.
*
* @return array
*/
protected function get_order_statuses() {
$order_statuses = array( 'auto-draft' );
foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
$order_statuses[] = str_replace( 'wc-', '', $status );
}
return $order_statuses;
}
/**
* Get the Order's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'parent_id' => array(
'description' => __( 'Parent order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'number' => array(
'description' => __( 'Order number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order_key' => array(
'description' => __( 'Order key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'created_via' => array(
'description' => __( 'Shows where the order was created.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'version' => array(
'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Order status.', 'woocommerce' ),
'type' => 'string',
'default' => 'pending',
'enum' => $this->get_order_statuses(),
'context' => array( 'view', 'edit' ),
),
'currency' => array(
'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ),
'type' => 'string',
'default' => get_woocommerce_currency(),
'enum' => array_keys( get_woocommerce_currencies() ),
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'discount_total' => array(
'description' => __( 'Total discount amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'discount_tax' => array(
'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_total' => array(
'description' => __( 'Total shipping amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax' => array(
'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'cart_tax' => array(
'description' => __( 'Sum of line item taxes only.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Grand total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Sum of all taxes.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'prices_include_tax' => array(
'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_id' => array(
'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ),
'type' => 'integer',
'default' => 0,
'context' => array( 'view', 'edit' ),
),
'customer_ip_address' => array(
'description' => __( "Customer's IP address.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_user_agent' => array(
'description' => __( 'User agent of the customer.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_note' => array(
'description' => __( 'Note left by customer during checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'billing' => array(
'description' => __( 'Billing address.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping' => array(
'description' => __( 'Shipping address.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'payment_method' => array(
'description' => __( 'Payment method ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'payment_method_title' => array(
'description' => __( 'Payment method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'transaction_id' => array(
'description' => __( 'Unique transaction ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_paid' => array(
'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_paid_gmt' => array(
'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_completed' => array(
'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_completed_gmt' => array(
'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'cart_hash' => array(
'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
'line_items' => array(
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'parent_name' => array(
'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'variation_id' => array(
'description' => __( 'Variation ID, if applicable.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'quantity' => array(
'description' => __( 'Quantity ordered.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class of product.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'subtotal' => array(
'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'subtotal_tax' => array(
'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'display_key' => array(
'description' => __( 'Meta key for UI display.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'display_value' => array(
'description' => __( 'Meta value for UI display.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'sku' => array(
'description' => __( 'Product SKU.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'price' => array(
'description' => __( 'Product price.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'image' => array(
'description' => __( 'Properties of the main product image.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
'tax_lines' => array(
'description' => __( 'Tax lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rate_code' => array(
'description' => __( 'Tax rate code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rate_id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Tax rate label.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'compound' => array(
'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tax_total' => array(
'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax_total' => array(
'description' => __( 'Shipping tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
),
'shipping_lines' => array(
'description' => __( 'Shipping lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_title' => array(
'description' => __( 'Shipping method name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'method_id' => array(
'description' => __( 'Shipping method ID.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'instance_id' => array(
'description' => __( 'Shipping instance ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
),
'fee_lines' => array(
'description' => __( 'Fee lines data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Fee name.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class of fee.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status of fee.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'enum' => array( 'taxable', 'none' ),
),
'total' => array(
'description' => __( 'Line total (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'total_tax' => array(
'description' => __( 'Line total tax (after discounts).', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Line taxes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Tax total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotal' => array(
'description' => __( 'Tax subtotal.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
),
'coupon_lines' => array(
'description' => __( 'Coupons line data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Item ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'discount' => array(
'description' => __( 'Discount total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'discount_tax' => array(
'description' => __( 'Discount total tax.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
),
),
'refunds' => array(
'description' => __( 'List of refunds.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Refund ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'reason' => array(
'description' => __( 'Refund reason.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Refund total.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'payment_url' => array(
'description' => __( 'Order payment URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'set_paid' => array(
'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'edit' ),
),
'is_editable' => array(
'description' => __( 'Whether an order can be edited.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'needs_payment' => array(
'description' => __( 'Whether an order needs payment, based on status and order total.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'needs_processing' => array(
'description' => __( 'Whether an order needs processing before it can be completed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['status'] = array(
'default' => 'any',
'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['customer'] = array(
'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product'] = array(
'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['dp'] = array(
'default' => wc_get_price_decimals(),
'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order_item_display_meta'] = array(
'default' => false,
'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'validate_callback' => 'rest_validate_request_arg',
);
$params['include_meta'] = array(
'default' => array(),
'description' => __( 'Limit meta_data to specific keys.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => 'wp_parse_list',
);
$params['exclude_meta'] = array(
'default' => array(),
'description' => __( 'Ensure meta_data excludes specific keys.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => 'wp_parse_list',
);
return $params;
}
/**
* Get objects.
*
* @param array $query_args Query args.
* @return array
*/
protected function get_objects( $query_args ) {
// Do not use WC_Order_Query for the CPT datastore.
if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
return parent::get_objects( $query_args );
}
$query = new \WC_Order_Query(
array_merge(
$query_args,
array(
'paginate' => true,
)
)
);
$results = $query->get_orders();
return array(
'objects' => $results->orders,
'total' => $results->total,
'pages' => $results->max_num_pages,
);
}
}
Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php 0000644 00000036731 15154275235 0022007 0 ustar 00 <?php
/**
* REST API WC Payment gateways controller
*
* Handles requests to the /payment_gateways endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Paymenga gateways controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Payment_Gateways_V2_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'payment_gateways';
/**
* Register the route for /payment_gateways and /payment_gateways/<id>
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to view payment gateways.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a payment gateway.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to edit payment gateways.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get payment gateways.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$payment_gateways = WC()->payment_gateways->payment_gateways();
$response = array();
foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) {
$payment_gateway->id = $payment_gateway_id;
$gateway = $this->prepare_item_for_response( $payment_gateway, $request );
$gateway = $this->prepare_response_for_collection( $gateway );
$response[] = $gateway;
}
return rest_ensure_response( $response );
}
/**
* Get a single payment gateway.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$gateway = $this->get_gateway( $request );
if ( is_null( $gateway ) ) {
return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
$gateway = $this->prepare_item_for_response( $gateway, $request );
return rest_ensure_response( $gateway );
}
/**
* Update A Single Payment Method.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function update_item( $request ) {
$gateway = $this->get_gateway( $request );
if ( is_null( $gateway ) ) {
return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
// Get settings.
$gateway->init_form_fields();
$settings = $gateway->settings;
// Update settings.
if ( isset( $request['settings'] ) ) {
$errors_found = false;
foreach ( $gateway->form_fields as $key => $field ) {
if ( isset( $request['settings'][ $key ] ) ) {
if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) {
$value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field );
} else {
$value = $this->validate_setting_text_field( $request['settings'][ $key ], $field );
}
if ( is_wp_error( $value ) ) {
$errors_found = true;
break;
}
$settings[ $key ] = $value;
}
}
if ( $errors_found ) {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
// Update if this method is enabled or not.
if ( isset( $request['enabled'] ) ) {
$settings['enabled'] = wc_bool_to_string( $request['enabled'] );
$gateway->enabled = $settings['enabled'];
}
// Update title.
if ( isset( $request['title'] ) ) {
$settings['title'] = $request['title'];
$gateway->title = $settings['title'];
}
// Update description.
if ( isset( $request['description'] ) ) {
$settings['description'] = $request['description'];
$gateway->description = $settings['description'];
}
// Update options.
$gateway->settings = $settings;
update_option( $gateway->get_option_key(), apply_filters( 'woocommerce_gateway_' . $gateway->id . '_settings_values', $settings, $gateway ) );
// Update order.
if ( isset( $request['order'] ) ) {
$order = (array) get_option( 'woocommerce_gateway_order' );
$order[ $gateway->id ] = $request['order'];
update_option( 'woocommerce_gateway_order', $order );
$gateway->order = absint( $request['order'] );
}
$gateway = $this->prepare_item_for_response( $gateway, $request );
return rest_ensure_response( $gateway );
}
/**
* Get a gateway based on the current request object.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|null
*/
public function get_gateway( $request ) {
$gateway = null;
$payment_gateways = WC()->payment_gateways->payment_gateways();
foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) {
if ( $request['id'] !== $payment_gateway_id ) {
continue;
}
$payment_gateway->id = $payment_gateway_id;
$gateway = $payment_gateway;
}
return $gateway;
}
/**
* Prepare a payment gateway for response.
*
* @param WC_Payment_Gateway $gateway Payment gateway object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $gateway, $request ) {
$order = (array) get_option( 'woocommerce_gateway_order' );
$item = array(
'id' => $gateway->id,
'title' => $gateway->title,
'description' => $gateway->description,
'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '',
'enabled' => ( 'yes' === $gateway->enabled ),
'method_title' => $gateway->get_method_title(),
'method_description' => $gateway->get_method_description(),
'settings' => $this->get_settings( $gateway ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $gateway, $request ) );
/**
* Filter payment gateway objects returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WC_Payment_Gateway $gateway Payment gateway object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request );
}
/**
* Return settings associated with this payment gateway.
*
* @param WC_Payment_Gateway $gateway Gateway data.
*
* @return array
*/
public function get_settings( $gateway ) {
$settings = array();
$gateway->init_form_fields();
foreach ( $gateway->form_fields as $id => $field ) {
// Make sure we at least have a title and type.
if ( empty( $field['title'] ) || empty( $field['type'] ) ) {
continue;
}
// Ignore 'title' settings/fields -- they are UI only.
if ( 'title' === $field['type'] ) {
continue;
}
// Ignore 'enabled' and 'description' which get included elsewhere.
if ( in_array( $id, array( 'enabled', 'description' ), true ) ) {
continue;
}
$data = array(
'id' => $id,
'label' => empty( $field['label'] ) ? $field['title'] : $field['label'],
'description' => empty( $field['description'] ) ? '' : $field['description'],
'type' => $field['type'],
'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ],
'default' => empty( $field['default'] ) ? '' : $field['default'],
'tip' => empty( $field['description'] ) ? '' : $field['description'],
'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
);
if ( ! empty( $field['options'] ) ) {
$data['options'] = $field['options'];
}
$settings[ $id ] = $data;
}
return $settings;
}
/**
* Prepare links for the request.
*
* @param WC_Payment_Gateway $gateway Payment gateway object.
* @param WP_REST_Request $request Request object.
* @return array
*/
protected function prepare_links( $gateway, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $gateway->id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the payment gateway schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'payment_gateway',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Payment gateway ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'order' => array(
'description' => __( 'Payment gateway sort order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'absint',
),
),
'enabled' => array(
'description' => __( 'Payment gateway enabled status.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
),
'method_title' => array(
'description' => __( 'Payment gateway method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_description' => array(
'description' => __( 'Payment gateway method description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'settings' => array(
'description' => __( 'Payment gateway settings.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Type of setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tip' => array(
'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'placeholder' => array(
'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get any query params needed.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php 0000644 00000001104 15154275236 0023304 0 ustar 00 <?php
/**
* REST API Product Attribute Terms controller
*
* Handles requests to the products/attributes/<attribute_id>/terms endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Attribute Terms controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Attribute_Terms_V1_Controller
*/
class WC_REST_Product_Attribute_Terms_V2_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php 0000644 00000001026 15154275236 0022342 0 ustar 00 <?php
/**
* REST API Product Attributes controller
*
* Handles requests to the products/attributes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Attributes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Attributes_V1_Controller
*/
class WC_REST_Product_Attributes_V2_Controller extends WC_REST_Product_Attributes_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-product-categories-v2-controller.php 0000644 00000016347 15154275237 0022316 0 ustar 00 <?php
/**
* REST API Product Categories controller
*
* Handles requests to the products/categories endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Categories controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Categories_V1_Controller
*/
class WC_REST_Product_Categories_V2_Controller extends WC_REST_Product_Categories_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Prepare a single product category output for response.
*
* @param WP_Term $item Term object.
* @param WP_REST_Request $request Request instance.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
// Get category display type.
$display_type = get_term_meta( $item->term_id, 'display_type', true );
// Get category order.
$menu_order = get_term_meta( $item->term_id, 'order', true );
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'parent' => (int) $item->parent,
'description' => $item->description,
'display' => $display_type ? $display_type : 'default',
'image' => null,
'menu_order' => (int) $menu_order,
'count' => (int) $item->count,
);
// Get category image.
$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
if ( $image_id ) {
$attachment = get_post( $image_id );
$data['image'] = array(
'id' => (int) $image_id,
'date_created' => wc_rest_prepare_date_response( $attachment->post_date ),
'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ),
'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
'src' => wp_get_attachment_url( $image_id ),
'title' => get_the_title( $attachment ),
'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Get the Category schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'parent' => array(
'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'display' => array(
'description' => __( 'Category archive display type.', 'woocommerce' ),
'type' => 'string',
'default' => 'default',
'enum' => array( 'default', 'products', 'subcategories', 'both' ),
'context' => array( 'view', 'edit' ),
),
'image' => array(
'description' => __( 'Image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'title' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php 0000644 00000014614 15154275240 0021642 0 ustar 00 <?php
/**
* REST API Product Reviews Controller
*
* Handles requests to /products/<product_id>/reviews.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Reviews Controller Class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Reviews_V1_Controller
*/
class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/(?P<product_id>[\d]+)/reviews';
/**
* Register the routes for product reviews.
*/
public function register_routes() {
parent::register_routes();
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/batch', array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check if a given request has access to batch manage product reviews.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Prepare a single product review output for response.
*
* @param WP_Comment $review Product review object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $review, $request ) {
$data = array(
'id' => (int) $review->comment_ID,
'date_created' => wc_rest_prepare_date_response( $review->comment_date ),
'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ),
'review' => $review->comment_content,
'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ),
'name' => $review->comment_author,
'email' => $review->comment_author_email,
'verified' => wc_review_is_from_verified_owner( $review->comment_ID ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $review, $request ) );
/**
* Filter product reviews object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $review Product review object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
}
/**
* Bulk create, update and delete items.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array Of WP_Error or WP_REST_Response.
*/
public function batch_items( $request ) {
$items = array_filter( $request->get_params() );
$params = $request->get_url_params();
$product_id = $params['product_id'];
$body_params = array();
foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) {
if ( ! empty( $items[ $batch_type ] ) ) {
$injected_items = array();
foreach ( $items[ $batch_type ] as $item ) {
$injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item;
}
$body_params[ $batch_type ] = $injected_items;
}
}
$request = new WP_REST_Request( $request->get_method() );
$request->set_body_params( $body_params );
return parent::batch_items( $request );
}
/**
* Get the Product Review's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_review',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'review' => array(
'description' => __( 'The content of the review.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_created_gmt' => array(
'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'rating' => array(
'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Reviewer name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Reviewer email.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'verified' => array(
'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php 0000644 00000001072 15154275240 0023424 0 ustar 00 <?php
/**
* REST API Product Shipping Classes controller
*
* Handles requests to the products/shipping_classes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Shipping Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Shipping_Classes_V1_Controller
*/
class WC_REST_Product_Shipping_Classes_V2_Controller extends WC_REST_Product_Shipping_Classes_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-product-tags-v2-controller.php 0000644 00000000762 15154275241 0021114 0 ustar 00 <?php
/**
* REST API Product Tags controller
*
* Handles requests to the products/tags endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Tags controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Tags_V1_Controller
*/
class WC_REST_Product_Tags_V2_Controller extends WC_REST_Product_Tags_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-product-variations-v2-controller.php 0000644 00000105557 15154275241 0022345 0 ustar 00 <?php
/**
* REST API variations controller
*
* Handles requests to the /products/<product_id>/variations endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API variations controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Products_V2_Controller
*/
class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/(?P<product_id>[\d]+)/variations';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'product_variation';
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
'id' => array(
'description' => __( 'Unique identifier for the variation.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param(
array(
'default' => 'view',
)
),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/batch', array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Get object.
*
* @since 3.0.0
* @param int $id Object ID.
* @return WC_Data
*/
protected function get_object( $id ) {
return wc_get_product( $id );
}
/**
* Check if a given request has access to update an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
// Check if variation belongs to the correct parent product.
if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Prepare a single variation output for response.
*
* @since 3.0.0
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$data = array(
'id' => $object->get_id(),
'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ),
'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ),
'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ),
'description' => wc_format_content( $object->get_description() ),
'permalink' => $object->get_permalink(),
'sku' => $object->get_sku(),
'price' => $object->get_price(),
'regular_price' => $object->get_regular_price(),
'sale_price' => $object->get_sale_price(),
'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ),
'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ),
'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ),
'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ),
'on_sale' => $object->is_on_sale(),
'visible' => $object->is_visible(),
'purchasable' => $object->is_purchasable(),
'virtual' => $object->is_virtual(),
'downloadable' => $object->is_downloadable(),
'downloads' => $this->get_downloads( $object ),
'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
'tax_status' => $object->get_tax_status(),
'tax_class' => $object->get_tax_class(),
'manage_stock' => $object->managing_stock(),
'stock_quantity' => $object->get_stock_quantity(),
'in_stock' => $object->is_in_stock(),
'backorders' => $object->get_backorders(),
'backorders_allowed' => $object->backorders_allowed(),
'backordered' => $object->is_on_backorder(),
'weight' => $object->get_weight(),
'dimensions' => array(
'length' => $object->get_length(),
'width' => $object->get_width(),
'height' => $object->get_height(),
),
'shipping_class' => $object->get_shipping_class(),
'shipping_class_id' => $object->get_shipping_class_id(),
'image' => current( $this->get_images( $object ) ),
'attributes' => $this->get_attributes( $object ),
'menu_order' => $object->get_menu_order(),
'meta_data' => $object->get_meta_data(),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
$args['post_parent'] = $request['product_id'];
return $args;
}
/**
* Prepare a single variation for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
if ( isset( $request['id'] ) ) {
$variation = wc_get_product( absint( $request['id'] ) );
} else {
$variation = new WC_Product_Variation();
}
// Update parent ID just once.
if ( 0 === $variation->get_parent_id() ) {
$variation->set_parent_id( absint( $request['product_id'] ) );
}
// Status.
if ( isset( $request['visible'] ) ) {
$variation->set_status( false === $request['visible'] ? 'private' : 'publish' );
}
// SKU.
if ( isset( $request['sku'] ) ) {
$variation->set_sku( wc_clean( $request['sku'] ) );
}
// Thumbnail.
if ( isset( $request['image'] ) ) {
if ( is_array( $request['image'] ) && ! empty( $request['image'] ) ) {
$image = $request['image'];
if ( is_array( $image ) ) {
$image['position'] = 0;
}
$variation = $this->set_product_images( $variation, array( $image ) );
} else {
$variation->set_image_id( '' );
}
}
// Virtual variation.
if ( isset( $request['virtual'] ) ) {
$variation->set_virtual( $request['virtual'] );
}
// Downloadable variation.
if ( isset( $request['downloadable'] ) ) {
$variation->set_downloadable( $request['downloadable'] );
}
// Downloads.
if ( $variation->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$variation = $this->save_downloadable_files( $variation, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$variation->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$variation->set_download_expiry( $request['download_expiry'] );
}
}
// Shipping data.
$variation = $this->save_product_shipping_data( $variation, $request );
// Stock handling.
if ( isset( $request['manage_stock'] ) ) {
if ( 'parent' === $request['manage_stock'] ) {
$variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does.
} else {
$variation->set_manage_stock( wc_string_to_bool( $request['manage_stock'] ) );
}
}
if ( isset( $request['in_stock'] ) ) {
$variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' );
}
if ( isset( $request['backorders'] ) ) {
$variation->set_backorders( $request['backorders'] );
}
if ( $variation->get_manage_stock() ) {
if ( isset( $request['stock_quantity'] ) ) {
$variation->set_stock_quantity( $request['stock_quantity'] );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$variation->set_stock_quantity( $stock_quantity );
}
} else {
$variation->set_backorders( 'no' );
$variation->set_stock_quantity( '' );
}
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$variation->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$variation->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$variation->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_from_gmt'] ) ) {
$variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$variation->set_date_on_sale_to( $request['date_on_sale_to'] );
}
if ( isset( $request['date_on_sale_to_gmt'] ) ) {
$variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
}
// Tax class.
if ( isset( $request['tax_class'] ) ) {
$variation->set_tax_class( $request['tax_class'] );
}
// Description.
if ( isset( $request['description'] ) ) {
$variation->set_description( wp_kses_post( $request['description'] ) );
}
// Update taxonomies.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
$parent = wc_get_product( $variation->get_parent_id() );
$parent_attributes = $parent->get_attributes();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$raw_attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $raw_attribute_name ) {
continue;
}
$attribute_name = sanitize_title( $raw_attribute_name );
if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
continue;
}
$attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine
if ( $term && ! is_wp_error( $term ) ) {
$attribute_value = $term->slug;
} else {
$attribute_value = sanitize_title( $attribute_value );
}
}
$attributes[ $attribute_key ] = $attribute_value;
}
$variation->set_attributes( $attributes );
}
// Menu order.
if ( $request['menu_order'] ) {
$variation->set_menu_order( $request['menu_order'] );
}
// Meta data.
if ( is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $variation Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
}
/**
* Clear caches here so in sync with any new variations.
*
* @param WC_Data $object Object data.
*/
public function clear_transients( $object ) {
wc_delete_product_transients( $object->get_parent_id() );
wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' );
}
/**
* Delete a variation.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return bool|WP_Error|WP_REST_Response
*/
public function delete_item( $request ) {
$force = (bool) $request['force'];
$object = $this->get_object( (int) $request['id'] );
$result = false;
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error(
"woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array(
'status' => 404,
)
);
}
$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );
/**
* Filter whether an object is trashable.
*
* Return false to disable trash support for the object.
*
* @param boolean $supports_trash Whether the object type support trashing.
* @param WC_Data $object The object being considered for trashing support.
*/
$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
return new WP_Error(
/* translators: %s: post type */
"woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array(
'status' => rest_authorization_required_code(),
)
);
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_object_for_response( $object, $request );
// If we're forcing, then delete permanently.
if ( $force ) {
$object->delete( true );
$result = 0 === $object->get_id();
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
return new WP_Error(
/* translators: %s: post type */
'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array(
'status' => 501,
)
);
}
// Otherwise, only trash if we haven't already.
if ( is_callable( array( $object, 'get_status' ) ) ) {
if ( 'trash' === $object->get_status() ) {
return new WP_Error(
/* translators: %s: post type */
'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array(
'status' => 410,
)
);
}
$object->delete();
$result = 'trash' === $object->get_status();
}
}
if ( ! $result ) {
return new WP_Error(
/* translators: %s: post type */
'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array(
'status' => 500,
)
);
}
// Delete parent product transients.
if ( 0 !== $object->get_parent_id() ) {
wc_delete_product_transients( $object->get_parent_id() );
}
/**
* Fires after a single object is deleted or trashed via the REST API.
*
* @param WC_Data $object The deleted or trashed object.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );
return $response;
}
/**
* Bulk create, update and delete items.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array Of WP_Error or WP_REST_Response.
*/
public function batch_items( $request ) {
$items = array_filter( $request->get_params() );
$params = $request->get_url_params();
$query = $request->get_query_params();
$product_id = $params['product_id'];
$body_params = array();
foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) {
if ( ! empty( $items[ $batch_type ] ) ) {
$injected_items = array();
foreach ( $items[ $batch_type ] as $item ) {
$injected_items[] = is_array( $item ) ? array_merge(
array(
'product_id' => $product_id,
), $item
) : $item;
}
$body_params[ $batch_type ] = $injected_items;
}
}
$request = new WP_REST_Request( $request->get_method() );
$request->set_body_params( $body_params );
$request->set_query_params( $query );
return parent::batch_items( $request );
}
/**
* Prepare links for the request.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {
$product_id = (int) $request['product_id'];
$base = str_replace( '(?P<product_id>[\d]+)', $product_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
'up' => array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ),
),
);
return $links;
}
/**
* Get the Variation's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) );
$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Variation description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Variation URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current variation price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Variation regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Variation sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from_gmt' => array(
'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to_gmt' => array(
'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'on_sale' => array(
'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'visible' => array(
'description' => __( "Define if the variation is visible on the product's page.", 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'purchasable' => array(
'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'virtual' => array(
'description' => __( 'If the variation is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at variation level.', 'woocommerce' ),
'type' => 'mixed',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(
'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Variation dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'image' => array(
'description' => __( 'Variation image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-products-v2-controller.php 0000644 00000234654 15154275243 0020356 0 ustar 00 <?php
/**
* REST API Products controller
*
* Handles requests to the /products endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API Products controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_CRUD_Controller
*/
class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products';
/**
* Post type.
*
* @var string
*/
protected $post_type = 'product';
/**
* If object is hierarchical.
*
* @var bool
*/
protected $hierarchical = true;
/**
* Initialize product actions.
*/
public function __construct() {
add_action( "woocommerce_rest_insert_{$this->post_type}_object", array( $this, 'clear_transients' ) );
}
/**
* Register the routes for products.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param(
array(
'default' => 'view',
)
),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
'type' => 'boolean',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Get object.
*
* @param int $id Object ID.
*
* @since 3.0.0
* @return WC_Data
*/
protected function get_object( $id ) {
return wc_get_product( $id );
}
/**
* Prepare a single product output for response.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*
* @since 3.0.0
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$this->request = $request;
$data = $this->get_product_data( $object, $context, $request );
// Add variations to variable products.
if ( $object->is_type( 'variable' ) && $object->has_child() ) {
$data['variations'] = $object->get_children();
}
// Add grouped products data.
if ( $object->is_type( 'grouped' ) && $object->has_child() ) {
$data['grouped_products'] = $object->get_children();
}
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare objects query.
*
* @param WP_REST_Request $request Full details about the request.
*
* @since 3.0.0
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
// Set post_status.
$args['post_status'] = $request['status'];
// Taxonomy query to filter products by type, category,
// tag, shipping class, and attribute.
$tax_query = array();
// Map between taxonomy name and arg's key.
$taxonomies = array(
'product_cat' => 'category',
'product_tag' => 'tag',
'product_shipping_class' => 'shipping_class',
);
// Set tax_query for each passed arg.
foreach ( $taxonomies as $taxonomy => $key ) {
if ( ! empty( $request[ $key ] ) ) {
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $request[ $key ],
);
}
}
// Filter product type by slug.
if ( ! empty( $request['type'] ) ) {
$tax_query[] = array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $request['type'],
);
}
// Filter by attribute and term.
if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
$tax_query[] = array(
'taxonomy' => $request['attribute'],
'field' => 'term_id',
'terms' => $request['attribute_term'],
);
}
}
if ( ! empty( $tax_query ) ) {
$args['tax_query'] = $tax_query; // WPCS: slow query ok.
}
// Filter featured.
if ( is_bool( $request['featured'] ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'featured',
'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
);
}
// Filter by sku.
if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $request['sku'];
}
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_sku',
'value' => $skus,
'compare' => 'IN',
)
);
}
// Filter by tax class.
if ( ! empty( $request['tax_class'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_tax_class',
'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
)
);
}
// Price filter.
if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok.
}
// Filter product in stock or out of stock.
if ( is_bool( $request['in_stock'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_stock_status',
'value' => true === $request['in_stock'] ? 'instock' : 'outofstock',
)
);
}
// Filter by on sale products.
if ( is_bool( $request['on_sale'] ) ) {
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
$on_sale_ids = wc_get_product_ids_on_sale();
// Use 0 when there's no on sale products to avoid return all products.
$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;
$args[ $on_sale_key ] += $on_sale_ids;
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = $this->post_type;
}
return $args;
}
/**
* Get the downloads for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
*
* @return array
*/
protected function get_downloads( $product ) {
$downloads = array();
if ( $product->is_downloadable() ) {
foreach ( $product->get_downloads() as $file_id => $file ) {
$downloads[] = array(
'id' => $file_id, // MD5 hash.
'name' => $file['name'],
'file' => $file['file'],
);
}
}
return $downloads;
}
/**
* Get taxonomy terms.
*
* @param WC_Product $product Product instance.
* @param string $taxonomy Taxonomy slug.
*
* @return array
*/
protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) {
$terms = array();
foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) {
$terms[] = array(
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
);
}
return $terms;
}
/**
* Get the images for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
*
* @return array
*/
protected function get_images( $product ) {
$images = array();
$attachment_ids = array();
// Add featured image.
if ( $product->get_image_id() ) {
$attachment_ids[] = $product->get_image_id();
}
// Add gallery images.
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
// Build image data.
foreach ( $attachment_ids as $position => $attachment_id ) {
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) ) {
continue;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
continue;
}
$images[] = array(
'id' => (int) $attachment_id,
'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
'src' => current( $attachment ),
'name' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
'position' => (int) $position,
);
}
// Set a placeholder image if the product has no images set.
if ( empty( $images ) ) {
$images[] = array(
'id' => 0,
'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now.
'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now.
'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ),
'date_modified_gmt' => wc_rest_prepare_date_response( time() ),
'src' => wc_placeholder_img_src(),
'name' => __( 'Placeholder', 'woocommerce' ),
'alt' => __( 'Placeholder', 'woocommerce' ),
'position' => 0,
);
}
return $images;
}
/**
* Get attribute taxonomy label.
*
* @param string $name Taxonomy name.
*
* @deprecated 3.0.0
* @return string
*/
protected function get_attribute_taxonomy_label( $name ) {
$tax = get_taxonomy( $name );
$labels = get_taxonomy_labels( $tax );
return $labels->singular_name;
}
/**
* Get product attribute taxonomy name.
*
* @param string $slug Taxonomy name.
* @param WC_Product $product Product data.
*
* @since 3.0.0
* @return string
*/
protected function get_attribute_taxonomy_name( $slug, $product ) {
// Format slug so it matches attributes of the product.
$slug = wc_attribute_taxonomy_slug( $slug );
$attributes = $product->get_attributes();
$attribute = false;
// pa_ attributes.
if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) {
$attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ];
} elseif ( isset( $attributes[ $slug ] ) ) {
$attribute = $attributes[ $slug ];
}
if ( ! $attribute ) {
return $slug;
}
// Taxonomy attribute name.
if ( $attribute->is_taxonomy() ) {
$taxonomy = $attribute->get_taxonomy_object();
return $taxonomy->attribute_label;
}
// Custom product attribute name.
return $attribute->get_name();
}
/**
* Get default attributes.
*
* @param WC_Product $product Product instance.
*
* @return array
*/
protected function get_default_attributes( $product ) {
$default = array();
if ( $product->is_type( 'variable' ) ) {
foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) {
if ( 0 === strpos( $key, 'pa_' ) ) {
$default[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $key ),
'name' => $this->get_attribute_taxonomy_name( $key, $product ),
'option' => $value,
);
} else {
$default[] = array(
'id' => 0,
'name' => $this->get_attribute_taxonomy_name( $key, $product ),
'option' => $value,
);
}
}
}
return $default;
}
/**
* Get attribute options.
*
* @param int $product_id Product ID.
* @param array $attribute Attribute data.
*
* @return array
*/
protected function get_attribute_options( $product_id, $attribute ) {
if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) {
return wc_get_product_terms(
$product_id,
$attribute['name'],
array(
'fields' => 'names',
)
);
} elseif ( isset( $attribute['value'] ) ) {
return array_map( 'trim', explode( '|', $attribute['value'] ) );
}
return array();
}
/**
* Get the attributes for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
*
* @return array
*/
protected function get_attributes( $product ) {
$attributes = array();
if ( $product->is_type( 'variation' ) ) {
$_product = wc_get_product( $product->get_parent_id() );
foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) {
$name = str_replace( 'attribute_', '', $attribute_name );
if ( empty( $attribute ) && '0' !== $attribute ) {
continue;
}
// Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`.
if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) {
$option_term = get_term_by( 'slug', $attribute, $name );
$attributes[] = array(
'id' => wc_attribute_taxonomy_id_by_name( $name ),
'name' => $this->get_attribute_taxonomy_name( $name, $_product ),
'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute,
);
} else {
$attributes[] = array(
'id' => 0,
'name' => $this->get_attribute_taxonomy_name( $name, $_product ),
'option' => $attribute,
);
}
}
} else {
foreach ( $product->get_attributes() as $attribute ) {
$attributes[] = array(
'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0,
'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ),
'position' => (int) $attribute['position'],
'visible' => (bool) $attribute['is_visible'],
'variation' => (bool) $attribute['is_variation'],
'options' => $this->get_attribute_options( $product->get_id(), $attribute ),
);
}
}
return $attributes;
}
/**
* Fetch price HTML.
*
* @param WC_Product $product Product object.
* @param string $context Context of request, can be `view` or `edit`.
*
* @return string
*/
protected function api_get_price_html( $product, $context ) {
return $product->get_price_html();
}
/**
* Fetch related IDs.
*
* @param WC_Product $product Product object.
* @param string $context Context of request, can be `view` or `edit`.
*
* @return array
*/
protected function api_get_related_ids( $product, $context ) {
return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) );
}
/**
* Fetch meta data.
*
* @param WC_Product $product Product object.
* @param string $context Context of request, can be `view` or `edit`.
*
* @return array
*/
protected function api_get_meta_data( $product, $context ) {
$meta_data = $product->get_meta_data();
if ( ! isset( $this->request ) || ! $this->request instanceof WP_REST_Request ) {
return $meta_data;
}
return $this->get_meta_data_for_response( $this->request, $meta_data );
}
/**
* Get product data.
*
* @param WC_Product $product Product instance.
* @param string $context Request context. Options: 'view' and 'edit'.
*
* @return array
*/
protected function get_product_data( $product, $context = 'view' ) {
/*
* @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently.
*
* TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean.
*/
$request = func_num_args() >= 3 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) );
$fields = $this->get_fields_for_response( $request );
$base_data = array();
foreach ( $fields as $field ) {
switch ( $field ) {
case 'id':
$base_data['id'] = $product->get_id();
break;
case 'name':
$base_data['name'] = $product->get_name( $context );
break;
case 'slug':
$base_data['slug'] = $product->get_slug( $context );
break;
case 'permalink':
$base_data['permalink'] = $product->get_permalink();
break;
case 'date_created':
$base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false );
break;
case 'date_created_gmt':
$base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) );
break;
case 'date_modified':
$base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false );
break;
case 'date_modified_gmt':
$base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) );
break;
case 'type':
$base_data['type'] = $product->get_type();
break;
case 'status':
$base_data['status'] = $product->get_status( $context );
break;
case 'featured':
$base_data['featured'] = $product->is_featured();
break;
case 'catalog_visibility':
$base_data['catalog_visibility'] = $product->get_catalog_visibility( $context );
break;
case 'description':
$base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context );
break;
case 'short_description':
$base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context );
break;
case 'sku':
$base_data['sku'] = $product->get_sku( $context );
break;
case 'price':
$base_data['price'] = $product->get_price( $context );
break;
case 'regular_price':
$base_data['regular_price'] = $product->get_regular_price( $context );
break;
case 'sale_price':
$base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : '';
break;
case 'date_on_sale_from':
$base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false );
break;
case 'date_on_sale_from_gmt':
$base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) );
break;
case 'date_on_sale_to':
$base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false );
break;
case 'date_on_sale_to_gmt':
$base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) );
break;
case 'on_sale':
$base_data['on_sale'] = $product->is_on_sale( $context );
break;
case 'purchasable':
$base_data['purchasable'] = $product->is_purchasable();
break;
case 'total_sales':
$base_data['total_sales'] = $product->get_total_sales( $context );
break;
case 'virtual':
$base_data['virtual'] = $product->is_virtual();
break;
case 'downloadable':
$base_data['downloadable'] = $product->is_downloadable();
break;
case 'downloads':
$base_data['downloads'] = $this->get_downloads( $product );
break;
case 'download_limit':
$base_data['download_limit'] = $product->get_download_limit( $context );
break;
case 'download_expiry':
$base_data['download_expiry'] = $product->get_download_expiry( $context );
break;
case 'external_url':
$base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : '';
break;
case 'button_text':
$base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : '';
break;
case 'tax_status':
$base_data['tax_status'] = $product->get_tax_status( $context );
break;
case 'tax_class':
$base_data['tax_class'] = $product->get_tax_class( $context );
break;
case 'manage_stock':
$base_data['manage_stock'] = $product->managing_stock();
break;
case 'stock_quantity':
$base_data['stock_quantity'] = $product->get_stock_quantity( $context );
break;
case 'in_stock':
$base_data['in_stock'] = $product->is_in_stock();
break;
case 'backorders':
$base_data['backorders'] = $product->get_backorders( $context );
break;
case 'backorders_allowed':
$base_data['backorders_allowed'] = $product->backorders_allowed();
break;
case 'backordered':
$base_data['backordered'] = $product->is_on_backorder();
break;
case 'low_stock_amount':
$base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount();
break;
case 'sold_individually':
$base_data['sold_individually'] = $product->is_sold_individually();
break;
case 'weight':
$base_data['weight'] = $product->get_weight( $context );
break;
case 'dimensions':
$base_data['dimensions'] = array(
'length' => $product->get_length( $context ),
'width' => $product->get_width( $context ),
'height' => $product->get_height( $context ),
);
break;
case 'shipping_required':
$base_data['shipping_required'] = $product->needs_shipping();
break;
case 'shipping_taxable':
$base_data['shipping_taxable'] = $product->is_shipping_taxable();
break;
case 'shipping_class':
$base_data['shipping_class'] = $product->get_shipping_class();
break;
case 'shipping_class_id':
$base_data['shipping_class_id'] = $product->get_shipping_class_id( $context );
break;
case 'reviews_allowed':
$base_data['reviews_allowed'] = $product->get_reviews_allowed( $context );
break;
case 'average_rating':
$base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context );
break;
case 'rating_count':
$base_data['rating_count'] = $product->get_rating_count();
break;
case 'upsell_ids':
$base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) );
break;
case 'cross_sell_ids':
$base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) );
break;
case 'parent_id':
$base_data['parent_id'] = $product->get_parent_id( $context );
break;
case 'purchase_note':
$base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context );
break;
case 'categories':
$base_data['categories'] = $this->get_taxonomy_terms( $product );
break;
case 'tags':
$base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' );
break;
case 'images':
$base_data['images'] = $this->get_images( $product );
break;
case 'attributes':
$base_data['attributes'] = $this->get_attributes( $product );
break;
case 'default_attributes':
$base_data['default_attributes'] = $this->get_default_attributes( $product );
break;
case 'variations':
$base_data['variations'] = array();
break;
case 'grouped_products':
$base_data['grouped_products'] = array();
break;
case 'menu_order':
$base_data['menu_order'] = $product->get_menu_order( $context );
break;
}
}
$data = array_merge(
$base_data,
$this->fetch_fields_using_getters( $product, $context, $fields )
);
return $data;
}
/**
* Prepare links for the request.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine.
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine.
),
);
if ( $object->get_parent_id() ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine.
);
}
return $links;
}
/**
* Prepare a single product for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $request['type'] ) ) {
$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );
if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}
$product = new $classname( $id );
} elseif ( isset( $request['id'] ) ) {
$product = wc_get_product( $id );
} else {
$product = new WC_Product_Simple();
}
if ( 'variation' === $product->get_type() ) {
return new WP_Error(
"woocommerce_rest_invalid_{$this->post_type}_id",
__( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ),
array(
'status' => 404,
)
);
}
// Post title.
if ( isset( $request['name'] ) ) {
$product->set_name( wp_filter_post_kses( $request['name'] ) );
}
// Post content.
if ( isset( $request['description'] ) ) {
$product->set_description( wp_filter_post_kses( $request['description'] ) );
}
// Post excerpt.
if ( isset( $request['short_description'] ) ) {
$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
}
// Post status.
if ( isset( $request['status'] ) ) {
$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
}
// Post slug.
if ( isset( $request['slug'] ) ) {
$product->set_slug( $request['slug'] );
}
// Menu order.
if ( isset( $request['menu_order'] ) ) {
$product->set_menu_order( $request['menu_order'] );
}
// Comment status.
if ( isset( $request['reviews_allowed'] ) ) {
$product->set_reviews_allowed( $request['reviews_allowed'] );
}
// Virtual.
if ( isset( $request['virtual'] ) ) {
$product->set_virtual( $request['virtual'] );
}
// Tax status.
if ( isset( $request['tax_status'] ) ) {
$product->set_tax_status( $request['tax_status'] );
}
// Tax Class.
if ( isset( $request['tax_class'] ) ) {
$product->set_tax_class( $request['tax_class'] );
}
// Catalog Visibility.
if ( isset( $request['catalog_visibility'] ) ) {
$product->set_catalog_visibility( $request['catalog_visibility'] );
}
// Purchase Note.
if ( isset( $request['purchase_note'] ) ) {
$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
}
// Featured Product.
if ( isset( $request['featured'] ) ) {
$product->set_featured( $request['featured'] );
}
// Shipping data.
$product = $this->save_product_shipping_data( $product, $request );
// SKU.
if ( isset( $request['sku'] ) ) {
$product->set_sku( wc_clean( $request['sku'] ) );
}
// Attributes.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = wc_clean( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( $attribute_id ) {
if ( isset( $attribute['options'] ) ) {
$options = $attribute['options'];
if ( ! is_array( $attribute['options'] ) ) {
// Text based attributes - Posted values are term names.
$options = explode( WC_DELIMITER, $options );
}
$values = array_map( 'wc_sanitize_term_text_based', $options );
$values = array_filter( $values, 'strlen' );
} else {
$values = array();
}
if ( ! empty( $values ) ) {
// Add attribute to array, but don't set values.
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_id( $attribute_id );
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
} elseif ( isset( $attribute['options'] ) ) {
// Custom attribute - Add attribute to array and set the values.
if ( is_array( $attribute['options'] ) ) {
$values = $attribute['options'];
} else {
$values = explode( WC_DELIMITER, $attribute['options'] );
}
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
}
$product->set_attributes( $attributes );
}
// Sales and prices.
if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
$product->set_regular_price( '' );
$product->set_sale_price( '' );
$product->set_date_on_sale_to( '' );
$product->set_date_on_sale_from( '' );
$product->set_price( '' );
} else {
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$product->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$product->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_from_gmt'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to'] );
}
if ( isset( $request['date_on_sale_to_gmt'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
}
}
// Product parent ID.
if ( isset( $request['parent_id'] ) ) {
$product->set_parent_id( $request['parent_id'] );
}
// Sold individually.
if ( isset( $request['sold_individually'] ) ) {
$product->set_sold_individually( $request['sold_individually'] );
}
// Stock status.
if ( isset( $request['in_stock'] ) ) {
$stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock';
} else {
$stock_status = $product->get_stock_status();
}
// Stock data.
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
// Manage stock.
if ( isset( $request['manage_stock'] ) ) {
$product->set_manage_stock( $request['manage_stock'] );
}
// Backorders.
if ( isset( $request['backorders'] ) ) {
$product->set_backorders( $request['backorders'] );
}
if ( $product->is_type( 'grouped' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
} elseif ( $product->is_type( 'external' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( 'instock' );
} elseif ( $product->get_manage_stock() ) {
// Stock status is always determined by children so sync later.
if ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Stock quantity.
if ( isset( $request['stock_quantity'] ) ) {
$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $product->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
}
} else {
// Don't manage stock.
$product->set_manage_stock( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
}
} elseif ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Upsells.
if ( isset( $request['upsell_ids'] ) ) {
$upsells = array();
$ids = $request['upsell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$upsells[] = $id;
}
}
}
$product->set_upsell_ids( $upsells );
}
// Cross sells.
if ( isset( $request['cross_sell_ids'] ) ) {
$crosssells = array();
$ids = $request['cross_sell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$crosssells[] = $id;
}
}
}
$product->set_cross_sell_ids( $crosssells );
}
// Product categories.
if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['categories'] );
}
// Product tags.
if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' );
}
// Downloadable.
if ( isset( $request['downloadable'] ) ) {
$product->set_downloadable( $request['downloadable'] );
}
// Downloadable options.
if ( $product->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$product = $this->save_downloadable_files( $product, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$product->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$product->set_download_expiry( $request['download_expiry'] );
}
}
// Product url and button text for external products.
if ( $product->is_type( 'external' ) ) {
if ( isset( $request['external_url'] ) ) {
$product->set_product_url( $request['external_url'] );
}
if ( isset( $request['button_text'] ) ) {
$product->set_button_text( $request['button_text'] );
}
}
// Save default attributes for variable products.
if ( $product->is_type( 'variable' ) ) {
$product = $this->save_default_attributes( $product, $request );
}
// Set children for a grouped product.
if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) {
$product->set_children( $request['grouped_products'] );
}
// Check for featured/gallery images, upload it and set it.
if ( isset( $request['images'] ) ) {
$product = $this->set_product_images( $product, $request['images'] );
}
// Allow set meta_data.
if ( is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $product Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating );
}
/**
* Set product images.
*
* @param WC_Product $product Product instance.
* @param array $images Images data.
*
* @throws WC_REST_Exception REST API exceptions.
* @return WC_Product
*/
protected function set_product_images( $product, $images ) {
$images = is_array( $images ) ? array_filter( $images ) : array();
if ( ! empty( $images ) ) {
$gallery_positions = array();
foreach ( $images as $index => $image ) {
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
if ( 0 === $attachment_id && isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
} else {
continue;
}
}
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {
/* translators: %s: attachment id */
throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
}
$gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index );
// Set the image alt if present.
if ( ! empty( $image['alt'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
}
// Set the image name if present.
if ( ! empty( $image['name'] ) ) {
wp_update_post(
array(
'ID' => $attachment_id,
'post_title' => $image['name'],
)
);
}
// Set the image source if present, for future reference.
if ( ! empty( $image['src'] ) ) {
update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) );
}
}
// Sort images and get IDs in correct order.
asort( $gallery_positions );
// Get gallery in correct order.
$gallery = array_keys( $gallery_positions );
// Featured image is in position 0.
$image_id = array_shift( $gallery );
// Set images.
$product->set_image_id( $image_id );
$product->set_gallery_image_ids( $gallery );
} else {
$product->set_image_id( '' );
$product->set_gallery_image_ids( array() );
}
return $product;
}
/**
* Save product shipping data.
*
* @param WC_Product $product Product instance.
* @param array $data Shipping data.
*
* @return WC_Product
*/
protected function save_product_shipping_data( $product, $data ) {
// Virtual.
if ( isset( $data['virtual'] ) && true === $data['virtual'] ) {
$product->set_weight( '' );
$product->set_height( '' );
$product->set_length( '' );
$product->set_width( '' );
} else {
if ( isset( $data['weight'] ) ) {
$product->set_weight( $data['weight'] );
}
// Height.
if ( isset( $data['dimensions']['height'] ) ) {
$product->set_height( $data['dimensions']['height'] );
}
// Width.
if ( isset( $data['dimensions']['width'] ) ) {
$product->set_width( $data['dimensions']['width'] );
}
// Length.
if ( isset( $data['dimensions']['length'] ) ) {
$product->set_length( $data['dimensions']['length'] );
}
}
// Shipping class.
if ( isset( $data['shipping_class'] ) ) {
$data_store = $product->get_data_store();
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
$product->set_shipping_class_id( $shipping_class_id );
}
return $product;
}
/**
* Save downloadable files.
*
* @param WC_Product $product Product instance.
* @param array $downloads Downloads data.
* @param int $deprecated Deprecated since 3.0.
*
* @return WC_Product
*/
protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) {
if ( $deprecated ) {
wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' );
}
$files = array();
foreach ( $downloads as $key => $file ) {
if ( empty( $file['file'] ) ) {
continue;
}
$download = new WC_Product_Download();
$download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() );
$download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) );
$download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) );
$files[] = $download;
}
$product->set_downloads( $files );
return $product;
}
/**
* Save taxonomy terms.
*
* @param WC_Product $product Product instance.
* @param array $terms Terms data.
* @param string $taxonomy Taxonomy name.
*
* @return WC_Product
*/
protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) {
$term_ids = wp_list_pluck( $terms, 'id' );
if ( 'cat' === $taxonomy ) {
$product->set_category_ids( $term_ids );
} elseif ( 'tag' === $taxonomy ) {
$product->set_tag_ids( $term_ids );
}
return $product;
}
/**
* Save default attributes.
*
* @param WC_Product $product Product instance.
* @param WP_REST_Request $request Request data.
*
* @since 3.0.0
* @return WC_Product
*/
protected function save_default_attributes( $product, $request ) {
if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) {
$attributes = $product->get_attributes();
$default_attributes = array();
foreach ( $request['default_attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( isset( $attributes[ $attribute_name ] ) ) {
$_attribute = $attributes[ $attribute_name ];
if ( $_attribute['is_variation'] ) {
$value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( ! empty( $_attribute['is_taxonomy'] ) ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $value, $attribute_name );
if ( $term && ! is_wp_error( $term ) ) {
$value = $term->slug;
} else {
$value = sanitize_title( $value );
}
}
if ( $value ) {
$default_attributes[ $attribute_name ] = $value;
}
}
}
}
$product->set_default_attributes( $default_attributes );
}
return $product;
}
/**
* Clear caches here so in sync with any new variations/children.
*
* @param WC_Data $object Object data.
*/
public function clear_transients( $object ) {
wc_delete_product_transients( $object->get_id() );
wp_cache_delete( 'product-' . $object->get_id(), 'products' );
}
/**
* Delete a single item.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$force = (bool) $request['force'];
$object = $this->get_object( (int) $request['id'] );
$result = false;
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error(
"woocommerce_rest_{$this->post_type}_invalid_id",
__( 'Invalid ID.', 'woocommerce' ),
array(
'status' => 404,
)
);
}
if ( 'variation' === $object->get_type() ) {
return new WP_Error(
"woocommerce_rest_invalid_{$this->post_type}_id",
__( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ),
array(
'status' => 404,
)
);
}
$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );
/**
* Filter whether an object is trashable.
*
* Return false to disable trash support for the object.
*
* @param boolean $supports_trash Whether the object type support trashing.
* @param WC_Data $object The object being considered for trashing support.
*/
$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
return new WP_Error(
"woocommerce_rest_user_cannot_delete_{$this->post_type}",
/* translators: %s: post type */
sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ),
array(
'status' => rest_authorization_required_code(),
)
);
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_object_for_response( $object, $request );
// If we're forcing, then delete permanently.
if ( $force ) {
if ( $object->is_type( 'variable' ) ) {
foreach ( $object->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! empty( $child ) ) {
$child->delete( true );
}
}
} else {
// For other product types, if the product has children, remove the relationship.
foreach ( $object->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! empty( $child ) ) {
$child->set_parent_id( 0 );
$child->save();
}
}
}
$object->delete( true );
$result = 0 === $object->get_id();
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
return new WP_Error(
'woocommerce_rest_trash_not_supported',
/* translators: %s: post type */
sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ),
array(
'status' => 501,
)
);
}
// Otherwise, only trash if we haven't already.
if ( is_callable( array( $object, 'get_status' ) ) ) {
if ( 'trash' === $object->get_status() ) {
return new WP_Error(
'woocommerce_rest_already_trashed',
/* translators: %s: post type */
sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ),
array(
'status' => 410,
)
);
}
$object->delete();
$result = 'trash' === $object->get_status();
}
}
if ( ! $result ) {
return new WP_Error(
'woocommerce_rest_cannot_delete',
/* translators: %s: post type */
sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ),
array(
'status' => 500,
)
);
}
// Delete parent product transients.
if ( 0 !== $object->get_parent_id() ) {
wc_delete_product_transients( $object->get_parent_id() );
}
/**
* Fires after a single object is deleted or trashed via the REST API.
*
* @param WC_Data $object The deleted or trashed object.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );
return $response;
}
/**
* Get the Product's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) );
$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'slug' => array(
'description' => __( 'Product slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Product type.', 'woocommerce' ),
'type' => 'string',
'default' => 'simple',
'enum' => array_keys( wc_get_product_types() ),
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Product status (post status).', 'woocommerce' ),
'type' => 'string',
'default' => 'publish',
'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ),
'context' => array( 'view', 'edit' ),
),
'featured' => array(
'description' => __( 'Featured product.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'catalog_visibility' => array(
'description' => __( 'Catalog visibility.', 'woocommerce' ),
'type' => 'string',
'default' => 'visible',
'enum' => array( 'visible', 'catalog', 'search', 'hidden' ),
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Product description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'short_description' => array(
'description' => __( 'Product short description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Product regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Product sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from_gmt' => array(
'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to_gmt' => array(
'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'price_html' => array(
'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'on_sale' => array(
'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'purchasable' => array(
'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_sales' => array(
'description' => __( 'Amount of sales.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'virtual' => array(
'description' => __( 'If the product is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the product is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'external_url' => array(
'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'button_text' => array(
'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at product level.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'in_stock' => array(
'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sold_individually' => array(
'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Product dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_required' => array(
'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_taxable' => array(
'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'reviews_allowed' => array(
'description' => __( 'Allow reviews.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'average_rating' => array(
'description' => __( 'Reviews average rating.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rating_count' => array(
'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'related_ids' => array(
'description' => __( 'List of related products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'upsell_ids' => array(
'description' => __( 'List of up-sell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'cross_sell_ids' => array(
'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'parent_id' => array(
'description' => __( 'Product parent ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'purchase_note' => array(
'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'categories' => array(
'description' => __( 'List of categories.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Category ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Category slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'tags' => array(
'description' => __( 'List of tags.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tag ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Tag name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Tag slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'images' => array(
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
),
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Attribute position.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'visible' => array(
'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'variation' => array(
'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'options' => array(
'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'string',
),
),
),
),
),
'default_attributes' => array(
'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'variations' => array(
'description' => __( 'List of variations IDs.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'integer',
),
'readonly' => true,
),
'grouped_products' => array(
'description' => __( 'List of grouped products ID.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) );
$params['slug'] = array(
'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['status'] = array(
'default' => 'any',
'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['type'] = array(
'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_types() ),
'sanitize_callback' => 'sanitize_key',
'validate_callback' => 'rest_validate_request_arg',
);
$params['sku'] = array(
'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['featured'] = array(
'description' => __( 'Limit result set to featured products.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['category'] = array(
'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['tag'] = array(
'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['shipping_class'] = array(
'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute'] = array(
'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['attribute_term'] = array(
'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
if ( wc_tax_enabled() ) {
$params['tax_class'] = array(
'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ),
'type' => 'string',
'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ),
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
}
$params['in_stock'] = array(
'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['on_sale'] = array(
'description' => __( 'Limit result set to products on sale.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
$params['min_price'] = array(
'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['max_price'] = array(
'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['include_meta'] = array(
'default' => array(),
'description' => __( 'Limit meta_data to specific keys.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => 'wp_parse_list',
);
$params['exclude_meta'] = array(
'default' => array(),
'description' => __( 'Ensure meta_data excludes specific keys.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'sanitize_callback' => 'wp_parse_list',
);
return $params;
}
}
Controllers/Version2/class-wc-rest-report-sales-v2-controller.php 0000644 00000000755 15154275246 0021127 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/sales endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Report Sales controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Report_Sales_V1_Controller
*/
class WC_REST_Report_Sales_V2_Controller extends WC_REST_Report_Sales_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php 0000644 00000001013 15154275246 0022255 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/top_sellers endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Report Top Sellers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Report_Top_Sellers_V1_Controller
*/
class WC_REST_Report_Top_Sellers_V2_Controller extends WC_REST_Report_Top_Sellers_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-reports-v2-controller.php 0000644 00000000723 15154275246 0020200 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_V1_Controller
*/
class WC_REST_Reports_V2_Controller extends WC_REST_Reports_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-setting-options-v2-controller.php 0000644 00000042603 15154275246 0021653 0 ustar 00 <?php
/**
* REST API Setting Options controller
*
* Handles requests to the /settings/$group/$setting endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Setting Options controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller {
/**
* WP REST API namespace/version.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'settings/(?P<group_id>[\w-]+)';
/**
* Register routes.
*
* @since 3.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
'args' => array(
'group' => array(
'description' => __( 'Settings group ID.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
'args' => array(
'group' => array(
'description' => __( 'Settings group ID.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\w-]+)',
array(
'args' => array(
'group' => array(
'description' => __( 'Settings group ID.', 'woocommerce' ),
'type' => 'string',
),
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Return a single setting.
*
* @since 3.0.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$setting = $this->get_setting( $request['group_id'], $request['id'] );
if ( is_wp_error( $setting ) ) {
return $setting;
}
$response = $this->prepare_item_for_response( $setting, $request );
return rest_ensure_response( $response );
}
/**
* Return all settings in a group.
*
* @since 3.0.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$settings = $this->get_group_settings( $request['group_id'] );
if ( is_wp_error( $settings ) ) {
return $settings;
}
$data = array();
foreach ( $settings as $setting_obj ) {
$setting = $this->prepare_item_for_response( $setting_obj, $request );
$setting = $this->prepare_response_for_collection( $setting );
if ( $this->is_setting_type_valid( $setting['type'] ) ) {
$data[] = $setting;
}
}
return rest_ensure_response( $data );
}
/**
* Get all settings in a group.
*
* @since 3.0.0
* @param string $group_id Group ID.
* @return array|WP_Error
*/
public function get_group_settings( $group_id ) {
if ( empty( $group_id ) ) {
return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
}
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() );
if ( empty( $settings ) ) {
return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
}
$filtered_settings = array();
foreach ( $settings as $setting ) {
$option_key = $setting['option_key'];
$setting = $this->filter_setting( $setting );
$default = isset( $setting['default'] ) ? $setting['default'] : '';
// Get the option value.
if ( is_array( $option_key ) ) {
$option = get_option( $option_key[0] );
$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
} else {
$admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default );
$setting['value'] = $admin_setting_value;
}
if ( 'multi_select_countries' === $setting['type'] ) {
$setting['options'] = WC()->countries->get_countries();
$setting['type'] = 'multiselect';
} elseif ( 'single_select_country' === $setting['type'] ) {
$setting['type'] = 'select';
$setting['options'] = $this->get_countries_and_states();
}
$filtered_settings[] = $setting;
}
return $filtered_settings;
}
/**
* Returns a list of countries and states for use in the base location setting.
*
* @since 3.0.7
* @return array Array of states and countries.
*/
private function get_countries_and_states() {
$countries = WC()->countries->get_countries();
if ( ! $countries ) {
return array();
}
$output = array();
foreach ( $countries as $key => $value ) {
$states = WC()->countries->get_states( $key );
if ( $states ) {
foreach ( $states as $state_key => $state_value ) {
$output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
}
} else {
$output[ $key ] = $value;
}
}
return $output;
}
/**
* Get setting data.
*
* @since 3.0.0
* @param string $group_id Group ID.
* @param string $setting_id Setting ID.
* @return stdClass|WP_Error
*/
public function get_setting( $group_id, $setting_id ) {
if ( empty( $setting_id ) ) {
return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
}
$settings = $this->get_group_settings( $group_id );
if ( is_wp_error( $settings ) ) {
return $settings;
}
$array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id, true );
if ( empty( $array_key ) ) {
return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
}
$setting = $settings[ $array_key[0] ];
if ( ! $this->is_setting_type_valid( $setting['type'] ) ) {
return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $setting;
}
/**
* Bulk create, update and delete items.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array Of WP_Error or WP_REST_Response.
*/
public function batch_items( $request ) {
// Get the request params.
$items = array_filter( $request->get_params() );
/*
* Since our batch settings update is group-specific and matches based on the route,
* we inject the URL parameters (containing group) into the batch items
*/
if ( ! empty( $items['update'] ) ) {
$to_update = array();
foreach ( $items['update'] as $item ) {
$to_update[] = array_merge( $request->get_url_params(), $item );
}
$request = new WP_REST_Request( $request->get_method() );
$request->set_body_params( array( 'update' => $to_update ) );
}
return parent::batch_items( $request );
}
/**
* Update a single setting in a group.
*
* @since 3.0.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$setting = $this->get_setting( $request['group_id'], $request['id'] );
if ( is_wp_error( $setting ) ) {
return $setting;
}
if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) {
$value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting );
} else {
$value = $this->validate_setting_text_field( $request['value'], $setting );
}
if ( is_wp_error( $value ) ) {
return $value;
}
if ( is_array( $setting['option_key'] ) ) {
$setting['value'] = $value;
$option_key = $setting['option_key'];
$prev = get_option( $option_key[0], null ) ?? array();
$prev[ $option_key[1] ] = $request['value'];
update_option( $option_key[0], $prev );
} else {
$update_data = array();
$update_data[ $setting['option_key'] ] = $value;
$setting['value'] = $value;
WC_Admin_Settings::save_fields( array( $setting ), $update_data );
}
$response = $this->prepare_item_for_response( $setting, $request );
return rest_ensure_response( $response );
}
/**
* Prepare a single setting object for response.
*
* @since 3.0.0
* @param object $item Setting object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
unset( $item['option_key'] );
$data = $this->filter_setting( $item );
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) );
return $response;
}
/**
* Prepare links for the request.
*
* @since 3.0.0
* @param string $setting_id Setting ID.
* @param string $group_id Group ID.
* @return array Links for the given setting.
*/
protected function prepare_links( $setting_id, $group_id ) {
$base = str_replace( '(?P<group_id>[\w-]+)', $group_id, $this->rest_base );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ),
),
);
return $links;
}
/**
* Makes sure the current user has access to READ the settings APIs.
*
* @since 3.0.0
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Makes sure the current user has access to WRITE the settings APIs.
*
* @since 3.0.0
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Filters out bad values from the settings array/filter so we
* only return known values via the API.
*
* @since 3.0.0
* @param array $setting Settings.
* @return array
*/
public function filter_setting( $setting ) {
$setting = array_intersect_key(
$setting,
array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) )
);
if ( empty( $setting['options'] ) ) {
unset( $setting['options'] );
}
if ( 'image_width' === $setting['type'] ) {
$setting = $this->cast_image_width( $setting );
}
return $setting;
}
/**
* For image_width, Crop can return "0" instead of false -- so we want
* to make sure we return these consistently the same we accept them.
*
* @todo remove in 4.0
* @since 3.0.0
* @param array $setting Settings.
* @return array
*/
public function cast_image_width( $setting ) {
foreach ( array( 'default', 'value' ) as $key ) {
if ( isset( $setting[ $key ] ) ) {
$setting[ $key ]['width'] = intval( $setting[ $key ]['width'] );
$setting[ $key ]['height'] = intval( $setting[ $key ]['height'] );
$setting[ $key ]['crop'] = (bool) $setting[ $key ]['crop'];
}
}
return $setting;
}
/**
* Callback for allowed keys for each setting response.
*
* @since 3.0.0
* @param string $key Key to check.
* @return boolean
*/
public function allowed_setting_keys( $key ) {
return in_array(
$key,
array(
'id',
'label',
'description',
'default',
'tip',
'placeholder',
'type',
'options',
'value',
'option_key',
),
true
);
}
/**
* Boolean for if a setting type is a valid supported setting type.
*
* @since 3.0.0
* @param string $type Type.
* @return bool
*/
public function is_setting_type_valid( $type ) {
return in_array(
$type,
array(
'text', // Validates with validate_setting_text_field.
'email', // Validates with validate_setting_text_field.
'number', // Validates with validate_setting_text_field.
'color', // Validates with validate_setting_text_field.
'password', // Validates with validate_setting_text_field.
'textarea', // Validates with validate_setting_textarea_field.
'select', // Validates with validate_setting_select_field.
'multiselect', // Validates with validate_setting_multiselect_field.
'radio', // Validates with validate_setting_radio_field (-> validate_setting_select_field).
'checkbox', // Validates with validate_setting_checkbox_field.
'image_width', // Validates with validate_setting_image_width_field.
'thumbnail_cropping', // Validates with validate_setting_text_field.
),
true
);
}
/**
* Get the settings schema, conforming to JSON Schema.
*
* @since 3.0.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'setting',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tip' => array(
'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'placeholder' => array(
'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Type of setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox', 'thumbnail_cropping' ),
'readonly' => true,
),
'options' => array(
'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-settings-v2-controller.php 0000644 00000014204 15154275246 0020341 0 ustar 00 <?php
/**
* REST API Settings controller
*
* Handles requests to the /settings endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Settings controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Settings_V2_Controller extends WC_REST_Controller {
/**
* WP REST API namespace/version.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'settings';
/**
* Register routes.
*
* @since 3.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get all settings groups items.
*
* @since 3.0.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$groups = apply_filters( 'woocommerce_settings_groups', array() );
if ( empty( $groups ) ) {
return new WP_Error( 'rest_setting_groups_empty', __( 'No setting groups have been registered.', 'woocommerce' ), array( 'status' => 500 ) );
}
$defaults = $this->group_defaults();
$filtered_groups = array();
foreach ( $groups as $group ) {
$sub_groups = array();
foreach ( $groups as $_group ) {
if ( ! empty( $_group['parent_id'] ) && $group['id'] === $_group['parent_id'] ) {
$sub_groups[] = $_group['id'];
}
}
$group['sub_groups'] = $sub_groups;
$group = wp_parse_args( $group, $defaults );
if ( ! is_null( $group['id'] ) && ! is_null( $group['label'] ) ) {
$group_obj = $this->filter_group( $group );
$group_data = $this->prepare_item_for_response( $group_obj, $request );
$group_data = $this->prepare_response_for_collection( $group_data );
$filtered_groups[] = $group_data;
}
}
$response = rest_ensure_response( $filtered_groups );
return $response;
}
/**
* Prepare links for the request.
*
* @param string $group_id Group ID.
* @return array Links for the given group.
*/
protected function prepare_links( $group_id ) {
$base = '/' . $this->namespace . '/' . $this->rest_base;
$links = array(
'options' => array(
'href' => rest_url( trailingslashit( $base ) . $group_id ),
),
);
return $links;
}
/**
* Prepare a report sales object for serialization.
*
* @since 3.0.0
* @param array $item Group object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item['id'] ) );
return $response;
}
/**
* Filters out bad values from the groups array/filter so we
* only return known values via the API.
*
* @since 3.0.0
* @param array $group Group.
* @return array
*/
public function filter_group( $group ) {
return array_intersect_key(
$group,
array_flip( array_filter( array_keys( $group ), array( $this, 'allowed_group_keys' ) ) )
);
}
/**
* Callback for allowed keys for each group response.
*
* @since 3.0.0
* @param string $key Key to check.
* @return boolean
*/
public function allowed_group_keys( $key ) {
return in_array( $key, array( 'id', 'label', 'description', 'parent_id', 'sub_groups' ) );
}
/**
* Returns default settings for groups. null means the field is required.
*
* @since 3.0.0
* @return array
*/
protected function group_defaults() {
return array(
'id' => null,
'label' => null,
'description' => '',
'parent_id' => '',
'sub_groups' => array(),
);
}
/**
* Makes sure the current user has access to READ the settings APIs.
*
* @since 3.0.0
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get the groups schema, conforming to JSON Schema.
*
* @since 3.0.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'setting_group',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'parent_id' => array(
'description' => __( 'ID of parent grouping.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'sub_groups' => array(
'description' => __( 'IDs for settings sub groups.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php 0000644 00000015432 15154275246 0021767 0 ustar 00 <?php
/**
* REST API WC Shipping Methods controller
*
* Handles requests to the /shipping_methods endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Shipping methods controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Shipping_Methods_V2_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'shipping_methods';
/**
* Register the route for /shipping_methods and /shipping_methods/<method>
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\w-]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to view shipping methods.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a shipping method.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get shipping methods.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$wc_shipping = WC_Shipping::instance();
$response = array();
foreach ( $wc_shipping->get_shipping_methods() as $id => $shipping_method ) {
$method = $this->prepare_item_for_response( $shipping_method, $request );
$method = $this->prepare_response_for_collection( $method );
$response[] = $method;
}
return rest_ensure_response( $response );
}
/**
* Get a single Shipping Method.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$wc_shipping = WC_Shipping::instance();
$methods = $wc_shipping->get_shipping_methods();
if ( empty( $methods[ $request['id'] ] ) ) {
return new WP_Error( 'woocommerce_rest_shipping_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
$method = $methods[ $request['id'] ];
$response = $this->prepare_item_for_response( $method, $request );
return rest_ensure_response( $response );
}
/**
* Prepare a shipping method for response.
*
* @param WC_Shipping_Method $method Shipping method object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $method, $request ) {
$data = array(
'id' => $method->id,
'title' => $method->method_title,
'description' => $method->method_description,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $method, $request ) );
/**
* Filter shipping methods object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WC_Shipping_Method $method Shipping method object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_shipping_method', $response, $method, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Shipping_Method $method Shipping method object.
* @param WP_REST_Request $request Request object.
* @return array
*/
protected function prepare_links( $method, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $method->id ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the shipping method schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'shipping_method',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Method ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Shipping method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Shipping method description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get any query params needed.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}
Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php 0000644 00000012247 15154275246 0023271 0 ustar 00 <?php
/**
* REST API Shipping Zone Locations controller
*
* Handles requests to the /shipping/zones/<id>/locations endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zone Locations class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zones_Controller_Base
*/
class WC_REST_Shipping_Zone_Locations_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {
/**
* Register the routes for Shipping Zone Locations.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/locations', array(
'args' => array(
'id' => array(
'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_items' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get all Shipping Zone Locations.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
$zone = $this->get_zone( (int) $request['id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$locations = $zone->get_zone_locations();
$data = array();
foreach ( $locations as $location_obj ) {
$location = $this->prepare_item_for_response( $location_obj, $request );
$location = $this->prepare_response_for_collection( $location );
$data[] = $location;
}
return rest_ensure_response( $data );
}
/**
* Update all Shipping Zone Locations.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function update_items( $request ) {
$zone = $this->get_zone( (int) $request['id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
if ( 0 === $zone->get_id() ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_locations_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) );
}
$raw_locations = $request->get_json_params();
$locations = array();
foreach ( (array) $raw_locations as $raw_location ) {
if ( empty( $raw_location['code'] ) ) {
continue;
}
$type = ! empty( $raw_location['type'] ) ? sanitize_text_field( $raw_location['type'] ) : 'country';
if ( ! in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true ) ) {
continue;
}
$locations[] = array(
'code' => sanitize_text_field( $raw_location['code'] ),
'type' => sanitize_text_field( $type ),
);
}
$zone->set_locations( $locations );
$zone->save();
return $this->get_items( $request );
}
/**
* Prepare the Shipping Zone Location for the REST response.
*
* @param array $item Shipping Zone Location.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( (int) $request['id'] ) );
return $response;
}
/**
* Prepare links for the request.
*
* @param int $zone_id Given Shipping Zone ID.
* @return array Links for the given Shipping Zone Location.
*/
protected function prepare_links( $zone_id ) {
$base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id;
$links = array(
'collection' => array(
'href' => rest_url( $base . '/locations' ),
),
'describes' => array(
'href' => rest_url( $base ),
),
);
return $links;
}
/**
* Get the Shipping Zone Locations schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'shipping_zone_location',
'type' => 'object',
'properties' => array(
'code' => array(
'description' => __( 'Shipping zone location code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'type' => array(
'description' => __( 'Shipping zone location type.', 'woocommerce' ),
'type' => 'string',
'default' => 'country',
'enum' => array(
'postcode',
'state',
'country',
'continent',
),
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php 0000644 00000042503 15154275246 0022737 0 ustar 00 <?php
/**
* REST API Shipping Zone Methods controller
*
* Handles requests to the /shipping/zones/<id>/methods endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zone Methods class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zones_Controller_Base
*/
class WC_REST_Shipping_Zone_Methods_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {
/**
* Register the routes for Shipping Zone Methods.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods', array(
'args' => array(
'zone_id' => array(
'description' => __( 'Unique ID for the zone.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'method_id' => array(
'required' => true,
'readonly' => false,
'description' => __( 'Shipping method ID.', 'woocommerce' ),
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<zone_id>[\d]+)/methods/(?P<instance_id>[\d]+)', array(
'args' => array(
'zone_id' => array(
'description' => __( 'Unique ID for the zone.', 'woocommerce' ),
'type' => 'integer',
),
'instance_id' => array(
'description' => __( 'Unique ID for the instance.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_items_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get a single Shipping Zone Method.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$zone = $this->get_zone( $request['zone_id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$instance_id = (int) $request['instance_id'];
$methods = $zone->get_shipping_methods();
$method = false;
foreach ( $methods as $method_obj ) {
if ( $instance_id === $method_obj->instance_id ) {
$method = $method_obj;
break;
}
}
if ( false === $method ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $method, $request );
return rest_ensure_response( $data );
}
/**
* Get all Shipping Zone Methods.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
$zone = $this->get_zone( $request['zone_id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$methods = $zone->get_shipping_methods();
$data = array();
foreach ( $methods as $method_obj ) {
$method = $this->prepare_item_for_response( $method_obj, $request );
$data[] = $method;
}
return rest_ensure_response( $data );
}
/**
* Create a new shipping zone method instance.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
$method_id = $request['method_id'];
$zone = $this->get_zone( $request['zone_id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$instance_id = $zone->add_shipping_method( $method_id );
$methods = $zone->get_shipping_methods();
$method = false;
foreach ( $methods as $method_obj ) {
if ( $instance_id === $method_obj->instance_id ) {
$method = $method_obj;
break;
}
}
if ( false === $method ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( 'Resource cannot be created.', 'woocommerce' ), array( 'status' => 500 ) );
}
$method = $this->update_fields( $instance_id, $method, $request );
if ( is_wp_error( $method ) ) {
return $method;
}
$data = $this->prepare_item_for_response( $method, $request );
return rest_ensure_response( $data );
}
/**
* Delete a shipping method instance.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item( $request ) {
$zone = $this->get_zone( $request['zone_id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$instance_id = (int) $request['instance_id'];
$force = $request['force'];
$methods = $zone->get_shipping_methods();
$method = false;
foreach ( $methods as $method_obj ) {
if ( $instance_id === $method_obj->instance_id ) {
$method = $method_obj;
break;
}
}
if ( false === $method ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
$method = $this->update_fields( $instance_id, $method, $request );
if ( is_wp_error( $method ) ) {
return $method;
}
$request->set_param( 'context', 'view' );
$response = $this->prepare_item_for_response( $method, $request );
// Actually delete.
if ( $force ) {
$zone->delete_shipping_method( $instance_id );
} else {
return new WP_Error( 'rest_trash_not_supported', __( 'Shipping methods do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
/**
* Fires after a product review is deleted via the REST API.
*
* @param object $method
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'rest_delete_product_review', $method, $response, $request );
return $response;
}
/**
* Update A Single Shipping Zone Method.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function update_item( $request ) {
$zone = $this->get_zone( $request['zone_id'] );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$instance_id = (int) $request['instance_id'];
$methods = $zone->get_shipping_methods();
$method = false;
foreach ( $methods as $method_obj ) {
if ( $instance_id === $method_obj->instance_id ) {
$method = $method_obj;
break;
}
}
if ( false === $method ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
$method = $this->update_fields( $instance_id, $method, $request );
if ( is_wp_error( $method ) ) {
return $method;
}
$data = $this->prepare_item_for_response( $method, $request );
return rest_ensure_response( $data );
}
/**
* Updates settings, order, and enabled status on create.
*
* @param int $instance_id Instance ID.
* @param WC_Shipping_Method $method Shipping method data.
* @param WP_REST_Request $request Request data.
*
* @return WC_Shipping_Method
*/
public function update_fields( $instance_id, $method, $request ) {
global $wpdb;
// Update settings if present.
if ( isset( $request['settings'] ) ) {
$method->init_instance_settings();
$instance_settings = $method->instance_settings;
$errors_found = false;
foreach ( $method->get_instance_form_fields() as $key => $field ) {
if ( isset( $request['settings'][ $key ] ) ) {
if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) {
$value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field );
} else {
$value = $this->validate_setting_text_field( $request['settings'][ $key ], $field );
}
if ( is_wp_error( $value ) ) {
$errors_found = true;
break;
}
$instance_settings[ $key ] = $value;
}
}
if ( $errors_found ) {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
update_option( $method->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $method->id . '_instance_settings_values', $instance_settings, $method ) );
}
// Update order.
if ( isset( $request['order'] ) ) {
$wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $request['order'] ) ), array( 'instance_id' => absint( $instance_id ) ) );
$method->method_order = absint( $request['order'] );
}
// Update if this method is enabled or not.
if ( isset( $request['enabled'] ) ) {
if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $request['enabled'] ), array( 'instance_id' => absint( $instance_id ) ) ) ) {
do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $request['zone_id'], $request['enabled'] );
$method->enabled = ( true === $request['enabled'] ? 'yes' : 'no' );
}
}
return $method;
}
/**
* Prepare the Shipping Zone Method for the REST response.
*
* @param array $item Shipping Zone Method.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$method = array(
'id' => $item->instance_id,
'instance_id' => $item->instance_id,
'title' => $item->instance_settings['title'],
'order' => $item->method_order,
'enabled' => ( 'yes' === $item->enabled ),
'method_id' => $item->id,
'method_title' => $item->method_title,
'method_description' => $item->method_description,
'settings' => $this->get_settings( $item ),
);
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$data = $this->add_additional_fields_to_object( $method, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $request['zone_id'], $item->instance_id ) );
$response = $this->prepare_response_for_collection( $response );
return $response;
}
/**
* Return settings associated with this shipping zone method instance.
*
* @param WC_Shipping_Method $item Shipping method data.
*
* @return array
*/
public function get_settings( $item ) {
$item->init_instance_settings();
$settings = array();
foreach ( $item->get_instance_form_fields() as $id => $field ) {
$data = array(
'id' => $id,
'label' => $field['title'],
'description' => empty( $field['description'] ) ? '' : $field['description'],
'type' => $field['type'],
'value' => $item->instance_settings[ $id ],
'default' => empty( $field['default'] ) ? '' : $field['default'],
'tip' => empty( $field['description'] ) ? '' : $field['description'],
'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
);
if ( ! empty( $field['options'] ) ) {
$data['options'] = $field['options'];
}
$settings[ $id ] = $data;
}
return $settings;
}
/**
* Prepare links for the request.
*
* @param int $zone_id Given Shipping Zone ID.
* @param int $instance_id Given Shipping Zone Method Instance ID.
* @return array Links for the given Shipping Zone Method.
*/
protected function prepare_links( $zone_id, $instance_id ) {
$base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id;
$links = array(
'self' => array(
'href' => rest_url( $base . '/methods/' . $instance_id ),
),
'collection' => array(
'href' => rest_url( $base . '/methods' ),
),
'describes' => array(
'href' => rest_url( $base ),
),
);
return $links;
}
/**
* Get the Shipping Zone Methods schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'shipping_zone_method',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Shipping method instance ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'instance_id' => array(
'description' => __( 'Shipping method instance ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Shipping method customer facing title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order' => array(
'description' => __( 'Shipping method sort order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'enabled' => array(
'description' => __( 'Shipping method enabled status.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
),
'method_id' => array(
'description' => __( 'Shipping method ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_title' => array(
'description' => __( 'Shipping method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_description' => array(
'description' => __( 'Shipping method description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'settings' => array(
'description' => __( 'Shipping method settings.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Type of setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tip' => array(
'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'placeholder' => array(
'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php 0000644 00000021046 15154275246 0021460 0 ustar 00 <?php
/**
* REST API Shipping Zones controller
*
* Handles requests to the /shipping/zones endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zones class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zones_Controller_Base
*/
class WC_REST_Shipping_Zones_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base {
/**
* Register the routes for Shipping Zones.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'name' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Shipping zone name.', 'woocommerce' ),
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique ID for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_items_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get a single Shipping Zone.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public function get_item( $request ) {
$zone = $this->get_zone( $request->get_param( 'id' ) );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$data = $zone->get_data();
$data = $this->prepare_item_for_response( $data, $request );
$data = $this->prepare_response_for_collection( $data );
return rest_ensure_response( $data );
}
/**
* Get all Shipping Zones.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response
*/
public function get_items( $request ) {
$rest_of_the_world = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 );
$zones = WC_Shipping_Zones::get_zones();
array_unshift( $zones, $rest_of_the_world->get_data() );
$data = array();
foreach ( $zones as $zone_obj ) {
$zone = $this->prepare_item_for_response( $zone_obj, $request );
$zone = $this->prepare_response_for_collection( $zone );
$data[] = $zone;
}
return rest_ensure_response( $data );
}
/**
* Create a single Shipping Zone.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
$zone = new WC_Shipping_Zone( null );
if ( ! is_null( $request->get_param( 'name' ) ) ) {
$zone->set_zone_name( $request->get_param( 'name' ) );
}
if ( ! is_null( $request->get_param( 'order' ) ) ) {
$zone->set_zone_order( $request->get_param( 'order' ) );
}
$zone->save();
if ( $zone->get_id() !== 0 ) {
$request->set_param( 'id', $zone->get_id() );
$response = $this->get_item( $request );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $zone->get_id() ) ) );
return $response;
} else {
return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( "Resource cannot be created. Check to make sure 'order' and 'name' are present.", 'woocommerce' ), array( 'status' => 500 ) );
}
}
/**
* Update a single Shipping Zone.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function update_item( $request ) {
$zone = $this->get_zone( $request->get_param( 'id' ) );
if ( is_wp_error( $zone ) ) {
return $zone;
}
if ( 0 === $zone->get_id() ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) );
}
$zone_changed = false;
if ( ! is_null( $request->get_param( 'name' ) ) ) {
$zone->set_zone_name( $request->get_param( 'name' ) );
$zone_changed = true;
}
if ( ! is_null( $request->get_param( 'order' ) ) ) {
$zone->set_zone_order( $request->get_param( 'order' ) );
$zone_changed = true;
}
if ( $zone_changed ) {
$zone->save();
}
return $this->get_item( $request );
}
/**
* Delete a single Shipping Zone.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function delete_item( $request ) {
$zone = $this->get_zone( $request->get_param( 'id' ) );
if ( is_wp_error( $zone ) ) {
return $zone;
}
$force = $request['force'];
$response = $this->get_item( $request );
if ( $force ) {
$zone->delete();
} else {
return new WP_Error( 'rest_trash_not_supported', __( 'Shipping zones do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
return $response;
}
/**
* Prepare the Shipping Zone for the REST response.
*
* @param array $item Shipping Zone.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $item, $request ) {
$data = array(
'id' => (int) $item['id'],
'name' => $item['zone_name'],
'order' => (int) $item['zone_order'],
);
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $data['id'] ) );
return $response;
}
/**
* Prepare links for the request.
*
* @param int $zone_id Given Shipping Zone ID.
* @return array Links for the given Shipping Zone.
*/
protected function prepare_links( $zone_id ) {
$base = '/' . $this->namespace . '/' . $this->rest_base;
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $zone_id ),
),
'collection' => array(
'href' => rest_url( $base ),
),
'describedby' => array(
'href' => rest_url( trailingslashit( $base ) . $zone_id . '/locations' ),
),
);
return $links;
}
/**
* Get the Shipping Zones schema, conforming to JSON Schema
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'shipping_zone',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Shipping zone name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'order' => array(
'description' => __( 'Shipping zone order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php 0000644 00000054417 15154275246 0022516 0 ustar 00 <?php
/**
* REST API WC System Status Tools Controller
*
* Handles requests to the /system_status/tools/* endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* System status tools controller.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_System_Status_Tools_V2_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'system_status/tools';
/**
* Register the routes for /system_status/tools/*.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\w-]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to view system status tools.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to view a specific system status tool.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to execute a specific system status tool.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* A list of available tools for use in the system status section.
* 'button' becomes 'action' in the API.
*
* @return array
*/
public function get_tools() {
$tools = array(
'clear_transients' => array(
'name' => __( 'WooCommerce transients', 'woocommerce' ),
'button' => __( 'Clear transients', 'woocommerce' ),
'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ),
),
'clear_expired_transients' => array(
'name' => __( 'Expired transients', 'woocommerce' ),
'button' => __( 'Clear transients', 'woocommerce' ),
'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ),
),
'delete_orphaned_variations' => array(
'name' => __( 'Orphaned variations', 'woocommerce' ),
'button' => __( 'Delete orphaned variations', 'woocommerce' ),
'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ),
),
'clear_expired_download_permissions' => array(
'name' => __( 'Used-up download permissions', 'woocommerce' ),
'button' => __( 'Clean up download permissions', 'woocommerce' ),
'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ),
),
'regenerate_product_lookup_tables' => array(
'name' => __( 'Product lookup tables', 'woocommerce' ),
'button' => __( 'Regenerate', 'woocommerce' ),
'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ),
),
'recount_terms' => array(
'name' => __( 'Term counts', 'woocommerce' ),
'button' => __( 'Recount terms', 'woocommerce' ),
'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ),
),
'reset_roles' => array(
'name' => __( 'Capabilities', 'woocommerce' ),
'button' => __( 'Reset capabilities', 'woocommerce' ),
'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ),
),
'clear_sessions' => array(
'name' => __( 'Clear customer sessions', 'woocommerce' ),
'button' => __( 'Clear', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' )
),
),
'clear_template_cache' => array(
'name' => __( 'Clear template cache', 'woocommerce' ),
'button' => __( 'Clear', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This tool will empty the template cache.', 'woocommerce' )
),
),
'install_pages' => array(
'name' => __( 'Create default WooCommerce pages', 'woocommerce' ),
'button' => __( 'Create pages', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' )
),
),
'delete_taxes' => array(
'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ),
'button' => __( 'Delete tax rates', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' )
),
),
'regenerate_thumbnails' => array(
'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ),
'button' => __( 'Regenerate', 'woocommerce' ),
'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ),
),
'db_update_routine' => array(
'name' => __( 'Update database', 'woocommerce' ),
'button' => __( 'Update database', 'woocommerce' ),
'desc' => sprintf(
'<strong class="red">%1$s</strong> %2$s',
__( 'Note:', 'woocommerce' ),
__( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' )
),
),
);
if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) {
$tools['verify_db_tables'] = array(
'name' => __( 'Verify base database tables', 'woocommerce' ),
'button' => __( 'Verify database', 'woocommerce' ),
'desc' => sprintf(
__( 'Verify if all base database tables are present.', 'woocommerce' )
),
);
}
// Jetpack does the image resizing heavy lifting so you don't have to.
if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) {
unset( $tools['regenerate_thumbnails'] );
}
if ( ! function_exists( 'wc_clear_template_cache' ) ) {
unset( $tools['clear_template_cache'] );
}
return apply_filters( 'woocommerce_debug_tools', $tools );
}
/**
* Get a list of system status tools.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$tools = array();
foreach ( $this->get_tools() as $id => $tool ) {
$tools[] = $this->prepare_response_for_collection(
$this->prepare_item_for_response(
array(
'id' => $id,
'name' => $tool['name'],
'action' => $tool['button'],
'description' => $tool['desc'],
),
$request
)
);
}
$response = rest_ensure_response( $tools );
return $response;
}
/**
* Return a single tool.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$tools = $this->get_tools();
if ( empty( $tools[ $request['id'] ] ) ) {
return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$tool = $tools[ $request['id'] ];
return rest_ensure_response(
$this->prepare_item_for_response(
array(
'id' => $request['id'],
'name' => $tool['name'],
'action' => $tool['button'],
'description' => $tool['desc'],
),
$request
)
);
}
/**
* Update (execute) a tool.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$tools = $this->get_tools();
if ( empty( $tools[ $request['id'] ] ) ) {
return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$tool = $tools[ $request['id'] ];
$tool = array(
'id' => $request['id'],
'name' => $tool['name'],
'action' => $tool['button'],
'description' => $tool['desc'],
);
$execute_return = $this->execute_tool( $request['id'] );
$tool = array_merge( $tool, $execute_return );
/**
* Fires after a WooCommerce REST system status tool has been executed.
*
* @param array $tool Details about the tool that has been executed.
* @param WP_REST_Request $request The current WP_REST_Request object.
*/
do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $tool, $request );
return rest_ensure_response( $response );
}
/**
* Prepare a tool item for serialization.
*
* @param array $item Object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$context = empty( $request['context'] ) ? 'view' : $request['context'];
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item['id'] ) );
return $response;
}
/**
* Get the system status tools schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'system_status_tool',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the tool.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'name' => array(
'description' => __( 'Tool name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'action' => array(
'description' => __( 'What running the tool will do.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'description' => array(
'description' => __( 'Tool description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'success' => array(
'description' => __( 'Did the tool run successfully?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'edit' ),
),
'message' => array(
'description' => __( 'Tool return message.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Prepare links for the request.
*
* @param string $id ID.
* @return array
*/
protected function prepare_links( $id ) {
$base = '/' . $this->namespace . '/' . $this->rest_base;
$links = array(
'item' => array(
'href' => rest_url( trailingslashit( $base ) . $id ),
'embeddable' => true,
),
);
return $links;
}
/**
* Get any query params needed.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Actually executes a tool.
*
* @param string $tool Tool.
* @return array
*/
public function execute_tool( $tool ) {
global $wpdb;
$ran = true;
switch ( $tool ) {
case 'clear_transients':
wc_delete_product_transients();
wc_delete_shop_order_transients();
delete_transient( 'wc_count_comments' );
delete_transient( 'as_comment_count' );
$attribute_taxonomies = wc_get_attribute_taxonomies();
if ( $attribute_taxonomies ) {
foreach ( $attribute_taxonomies as $attribute ) {
delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name );
}
}
WC_Cache_Helper::get_transient_version( 'shipping', true );
$message = __( 'Product transients cleared', 'woocommerce' );
break;
case 'clear_expired_transients':
/* translators: %d: amount of expired transients */
$message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() );
break;
case 'delete_orphaned_variations':
// Delete orphans.
$result = absint(
$wpdb->query(
"DELETE products
FROM {$wpdb->posts} products
LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent
WHERE wp.ID IS NULL AND products.post_type = 'product_variation';"
)
);
/* translators: %d: amount of orphaned variations */
$message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result );
break;
case 'clear_expired_download_permissions':
// Delete related records in wc_download_log (aka ON DELETE CASCADE).
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wc_download_log
WHERE permission_id IN (
SELECT permission_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )
)",
current_time( 'Y-m-d' )
)
);
// Delete expired download permissions and ones with 0 downloads remaining.
$result = absint(
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions
WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )",
current_time( 'Y-m-d' )
)
)
);
/* translators: %d: amount of permissions */
$message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result );
break;
case 'regenerate_product_lookup_tables':
if ( ! wc_update_product_lookup_tables_is_running() ) {
wc_update_product_lookup_tables();
}
$message = __( 'Lookup tables are regenerating', 'woocommerce' );
break;
case 'reset_roles':
// Remove then re-add caps and roles.
WC_Install::remove_roles();
WC_Install::create_roles();
$message = __( 'Roles successfully reset', 'woocommerce' );
break;
case 'recount_terms':
wc_recount_all_terms();
$message = __( 'Terms successfully recounted', 'woocommerce' );
break;
case 'clear_sessions':
$wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" );
$result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok.
wp_cache_flush();
/* translators: %d: amount of sessions */
$message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) );
break;
case 'install_pages':
WC_Install::create_pages();
$message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' );
break;
case 'delete_taxes':
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" );
$wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" );
if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) {
WC_Cache_Helper::invalidate_cache_group( 'taxes' );
} else {
WC_Cache_Helper::incr_cache_prefix( 'taxes' );
}
$message = __( 'Tax rates successfully deleted', 'woocommerce' );
break;
case 'regenerate_thumbnails':
WC_Regenerate_Images::queue_image_regeneration();
$message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' );
break;
case 'db_update_routine':
$blog_id = get_current_blog_id();
// Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck().
// This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running.
do_action( 'wp_' . $blog_id . '_wc_updater_cron' );
$message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' );
break;
case 'clear_template_cache':
if ( function_exists( 'wc_clear_template_cache' ) ) {
wc_clear_template_cache();
$message = __( 'Template cache cleared.', 'woocommerce' );
} else {
$message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' );
$ran = false;
}
break;
case 'verify_db_tables':
if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) {
$message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' );
$ran = false;
break;
}
// Try to manually create table again.
$missing_tables = WC_Install::verify_base_tables( true, true );
if ( 0 === count( $missing_tables ) ) {
$message = __( 'Database verified successfully.', 'woocommerce' );
} else {
$message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' );
$message .= implode( ', ', $missing_tables );
$ran = false;
}
break;
default:
$tools = $this->get_tools();
if ( isset( $tools[ $tool ]['callback'] ) ) {
$callback = $tools[ $tool ]['callback'];
try {
$return = call_user_func( $callback );
} catch ( Exception $exception ) {
$return = $exception;
}
if ( is_a( $return, Exception::class ) ) {
$callback_string = $this->get_printable_callback_name( $callback, $tool );
$ran = false;
/* translators: %1$s: callback string, %2$s: error message */
$message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() );
$logger = wc_get_logger();
$logger->error(
sprintf(
'Error running debug tool %s: %s',
$tool,
$return->getMessage()
),
array(
'source' => 'run-debug-tool',
'tool' => $tool,
'callback' => $callback,
'error' => $return,
)
);
} elseif ( is_string( $return ) ) {
$message = $return;
} elseif ( false === $return ) {
$callback_string = $this->get_printable_callback_name( $callback, $tool );
$ran = false;
/* translators: %s: callback string */
$message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string );
} else {
$message = __( 'Tool ran.', 'woocommerce' );
}
} else {
$ran = false;
$message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' );
}
break;
}
return array(
'success' => $ran,
'message' => $message,
);
}
/**
* Get a printable name for a callback.
*
* @param mixed $callback The callback to get a name for.
* @param string $default The default name, to be returned when the callback is an inline function.
* @return string A printable name for the callback.
*/
private function get_printable_callback_name( $callback, $default ) {
if ( is_array( $callback ) ) {
return get_class( $callback[0] ) . '::' . $callback[1];
}
if ( is_string( $callback ) ) {
return $callback;
}
return $default;
}
}
Controllers/Version2/class-wc-rest-system-status-v2-controller.php 0000644 00000142440 15154275246 0021352 0 ustar 00 <?php
/**
* REST API WC System Status controller
*
* Handles requests to the /system_status endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer as Order_DataSynchronizer;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* System status controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_System_Status_V2_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'system_status';
/**
* Register cache cleaner
*
* Handles all the cache cleaning for this endpoint. We need to register
* these functions before the routes are registered, so this function gets
* called from Server.php
*/
public static function register_cache_clean() {
// Clear the theme cache if we switch themes or our theme is upgraded.
add_action( 'switch_theme', array( __CLASS__, 'clean_theme_cache' ) );
add_action( 'activate_plugin', array( __CLASS__, 'clean_plugin_cache' ) );
add_action( 'deactivate_plugin', array( __CLASS__, 'clean_plugin_cache' ) );
add_action(
'upgrader_process_complete',
function( $upgrader, $extra ) {
if ( ! $extra || ! $extra['type'] ) {
return;
}
// Clear the cache if woocommerce is updated.
if ( 'plugin' === $extra['type'] ) {
\WC_REST_System_Status_V2_Controller::clean_theme_cache();
\WC_REST_System_Status_V2_Controller::clean_plugin_cache();
return;
}
if ( 'theme' === $extra['type'] ) {
\WC_REST_System_Status_V2_Controller::clean_theme_cache();
return;
}
},
10,
2
);
}
/**
* Register the route for /system_status
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to view system status.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get a system status info, by section.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$fields = $this->get_fields_for_response( $request );
$mappings = $this->get_item_mappings_per_fields( $fields );
$response = $this->prepare_item_for_response( $mappings, $request );
return rest_ensure_response( $response );
}
/**
* Get the system status schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'system_status',
'type' => 'object',
'properties' => array(
'environment' => array(
'description' => __( 'Environment.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'home_url' => array(
'description' => __( 'Home URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
'site_url' => array(
'description' => __( 'Site URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
'version' => array(
'description' => __( 'WooCommerce version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'log_directory' => array(
'description' => __( 'Log directory.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'log_directory_writable' => array(
'description' => __( 'Is log directory writable?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'wp_version' => array(
'description' => __( 'WordPress version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'wp_multisite' => array(
'description' => __( 'Is WordPress multisite?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'wp_memory_limit' => array(
'description' => __( 'WordPress memory limit.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'wp_debug_mode' => array(
'description' => __( 'Is WordPress debug mode active?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'wp_cron' => array(
'description' => __( 'Are WordPress cron jobs enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'language' => array(
'description' => __( 'WordPress language.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'server_info' => array(
'description' => __( 'Server info.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'php_version' => array(
'description' => __( 'PHP version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'php_post_max_size' => array(
'description' => __( 'PHP post max size.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'php_max_execution_time' => array(
'description' => __( 'PHP max execution time.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'php_max_input_vars' => array(
'description' => __( 'PHP max input vars.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'curl_version' => array(
'description' => __( 'cURL version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'suhosin_installed' => array(
'description' => __( 'Is SUHOSIN installed?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'max_upload_size' => array(
'description' => __( 'Max upload size.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'mysql_version' => array(
'description' => __( 'MySQL version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'mysql_version_string' => array(
'description' => __( 'MySQL version string.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'default_timezone' => array(
'description' => __( 'Default timezone.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'fsockopen_or_curl_enabled' => array(
'description' => __( 'Is fsockopen/cURL enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'soapclient_enabled' => array(
'description' => __( 'Is SoapClient class enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'domdocument_enabled' => array(
'description' => __( 'Is DomDocument class enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'gzip_enabled' => array(
'description' => __( 'Is GZip enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'mbstring_enabled' => array(
'description' => __( 'Is mbstring enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'remote_post_successful' => array(
'description' => __( 'Remote POST successful?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'remote_post_response' => array(
'description' => __( 'Remote POST response.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'remote_get_successful' => array(
'description' => __( 'Remote GET successful?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'remote_get_response' => array(
'description' => __( 'Remote GET response.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
'database' => array(
'description' => __( 'Database.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'wc_database_version' => array(
'description' => __( 'WC database version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'database_prefix' => array(
'description' => __( 'Database prefix.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'maxmind_geoip_database' => array(
'description' => __( 'MaxMind GeoIP database.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'database_tables' => array(
'description' => __( 'Database tables.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
),
),
'active_plugins' => array(
'description' => __( 'Active plugins.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'inactive_plugins' => array(
'description' => __( 'Inactive plugins.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'dropins_mu_plugins' => array(
'description' => __( 'Dropins & MU plugins.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'theme' => array(
'description' => __( 'Theme.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'name' => array(
'description' => __( 'Theme name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'version' => array(
'description' => __( 'Theme version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'version_latest' => array(
'description' => __( 'Latest version of theme.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'author_url' => array(
'description' => __( 'Theme author URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
'is_child_theme' => array(
'description' => __( 'Is this theme a child theme?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'has_woocommerce_support' => array(
'description' => __( 'Does the theme declare WooCommerce support?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'has_woocommerce_file' => array(
'description' => __( 'Does the theme have a woocommerce.php file?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'has_outdated_templates' => array(
'description' => __( 'Does this theme have outdated templates?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'overrides' => array(
'description' => __( 'Template overrides.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'parent_name' => array(
'description' => __( 'Parent theme name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'parent_version' => array(
'description' => __( 'Parent theme version.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'parent_author_url' => array(
'description' => __( 'Parent theme author URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
'settings' => array(
'description' => __( 'Settings.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'api_enabled' => array(
'description' => __( 'REST API enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'force_ssl' => array(
'description' => __( 'SSL forced?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'currency' => array(
'description' => __( 'Currency.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'currency_symbol' => array(
'description' => __( 'Currency symbol.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'currency_position' => array(
'description' => __( 'Currency position.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'thousand_separator' => array(
'description' => __( 'Thousand separator.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'decimal_separator' => array(
'description' => __( 'Decimal separator.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'number_of_decimals' => array(
'description' => __( 'Number of decimals.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'geolocation_enabled' => array(
'description' => __( 'Geolocation enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'taxonomies' => array(
'description' => __( 'Taxonomy terms for product/order statuses.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'product_visibility_terms' => array(
'description' => __( 'Terms in the product visibility taxonomy.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'wccom_connected' => array(
'description' => __( 'Is store connected to WooCommerce.com?', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'enforce_approved_download_dirs' => array(
'description' => __( 'Enforce approved download directories?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'HPOS_feature_screen_enabled' => array(
'description' => __( 'Is HPOS feature screen enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'HPOS_enabled' => array(
'description' => __( 'Is HPOS enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'order_datastore' => array(
'description' => __( 'Order datastore.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'HPOS_sync_enabled' => array(
'description' => __( 'Is HPOS sync enabled?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
'security' => array(
'description' => __( 'Security.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'secure_connection' => array(
'description' => __( 'Is the connection to your store secure?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
'hide_errors' => array(
'description' => __( 'Hide errors from visitors?', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view' ),
'readonly' => true,
),
),
),
'pages' => array(
'description' => __( 'WooCommerce pages.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'post_type_counts' => array(
'description' => __( 'Total post count.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Return an array of sections and the data associated with each.
*
* @deprecated 3.9.0
* @return array
*/
public function get_item_mappings() {
return array(
'environment' => $this->get_environment_info(),
'database' => $this->get_database_info(),
'active_plugins' => $this->get_active_plugins(),
'inactive_plugins' => $this->get_inactive_plugins(),
'dropins_mu_plugins' => $this->get_dropins_mu_plugins(),
'theme' => $this->get_theme_info(),
'settings' => $this->get_settings(),
'security' => $this->get_security_info(),
'pages' => $this->get_pages(),
'post_type_counts' => $this->get_post_type_counts(),
);
}
/**
* Return an array of sections and the data associated with each.
*
* @since 3.9.0
* @param array $fields List of fields to be included on the response.
* @return array
*/
public function get_item_mappings_per_fields( $fields ) {
$items = array();
foreach ( $fields as $field ) {
// If we're looking for a sub-property, like environment.version we need
// to extract the first-level property here so we know which function to run.
list( $prop ) = explode( '.', $field, 2 );
switch ( $prop ) {
case 'environment':
$items['environment'] = $this->get_environment_info_per_fields( $fields );
break;
case 'database':
$items['database'] = $this->get_database_info();
break;
case 'active_plugins':
$items['active_plugins'] = $this->get_active_plugins();
break;
case 'inactive_plugins':
$items['inactive_plugins'] = $this->get_inactive_plugins();
break;
case 'dropins_mu_plugins':
$items['dropins_mu_plugins'] = $this->get_dropins_mu_plugins();
break;
case 'theme':
$items['theme'] = $this->get_theme_info();
break;
case 'settings':
$items['settings'] = $this->get_settings();
break;
case 'security':
$items['security'] = $this->get_security_info();
break;
case 'pages':
$items['pages'] = $this->get_pages();
break;
case 'post_type_counts':
$items['post_type_counts'] = $this->get_post_type_counts();
break;
}
}
return $items;
}
/**
* Get array of environment information. Includes thing like software
* versions, and various server settings.
*
* @deprecated 3.9.0
* @return array
*/
public function get_environment_info() {
return $this->get_environment_info_per_fields( array( 'environment' ) );
}
/**
* Check if field item exists.
*
* @since 3.9.0
* @param string $section Fields section.
* @param array $items List of items to check for.
* @param array $fields List of fields to be included on the response.
* @return bool
*/
private function check_if_field_item_exists( $section, $items, $fields ) {
if ( ! in_array( $section, $fields, true ) ) {
return false;
}
$exclude = array();
foreach ( $fields as $field ) {
$values = explode( '.', $field );
if ( $section !== $values[0] || empty( $values[1] ) ) {
continue;
}
$exclude[] = $values[1];
}
return 0 <= count( array_intersect( $items, $exclude ) );
}
/**
* Get array of environment information. Includes thing like software
* versions, and various server settings.
*
* @param array $fields List of fields to be included on the response.
* @return array
*/
public function get_environment_info_per_fields( $fields ) {
global $wpdb;
$enable_remote_post = $this->check_if_field_item_exists( 'environment', array( 'remote_post_successful', 'remote_post_response' ), $fields );
$enable_remote_get = $this->check_if_field_item_exists( 'environment', array( 'remote_get_successful', 'remote_get_response' ), $fields );
// Figure out cURL version, if installed.
$curl_version = '';
if ( function_exists( 'curl_version' ) ) {
$curl_version = curl_version();
$curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version'];
} elseif ( extension_loaded( 'curl' ) ) {
$curl_version = __( 'cURL installed but unable to retrieve version.', 'woocommerce' );
}
// WP memory limit.
$wp_memory_limit = wc_let_to_num( WP_MEMORY_LIMIT );
if ( function_exists( 'memory_get_usage' ) ) {
$wp_memory_limit = max( $wp_memory_limit, wc_let_to_num( @ini_get( 'memory_limit' ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
}
// Test POST requests.
$post_response_successful = null;
$post_response_code = null;
if ( $enable_remote_post ) {
$post_response_code = get_transient( 'woocommerce_test_remote_post' );
if ( false === $post_response_code || is_wp_error( $post_response_code ) ) {
$response = wp_safe_remote_post(
'https://www.paypal.com/cgi-bin/webscr',
array(
'timeout' => 10,
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
'httpversion' => '1.1',
'body' => array(
'cmd' => '_notify-validate',
),
)
);
if ( ! is_wp_error( $response ) ) {
$post_response_code = $response['response']['code'];
}
set_transient( 'woocommerce_test_remote_post', $post_response_code, HOUR_IN_SECONDS );
}
$post_response_successful = ! is_wp_error( $post_response_code ) && $post_response_code >= 200 && $post_response_code < 300;
}
// Test GET requests.
$get_response_successful = null;
$get_response_code = null;
if ( $enable_remote_get ) {
$get_response_code = get_transient( 'woocommerce_test_remote_get' );
if ( false === $get_response_code || is_wp_error( $get_response_code ) ) {
$response = wp_safe_remote_get(
'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ),
array(
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
)
);
if ( ! is_wp_error( $response ) ) {
$get_response_code = $response['response']['code'];
}
set_transient( 'woocommerce_test_remote_get', $get_response_code, HOUR_IN_SECONDS );
}
$get_response_successful = ! is_wp_error( $get_response_code ) && $get_response_code >= 200 && $get_response_code < 300;
}
$database_version = wc_get_server_database_version();
// Return all environment info. Described by JSON Schema.
return array(
'home_url' => get_option( 'home' ),
'site_url' => get_option( 'siteurl' ),
'version' => WC()->version,
'log_directory' => WC_LOG_DIR,
'log_directory_writable' => (bool) @fopen( WC_LOG_DIR . 'test-log.log', 'a' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen
'wp_version' => get_bloginfo( 'version' ),
'wp_multisite' => is_multisite(),
'wp_memory_limit' => $wp_memory_limit,
'wp_debug_mode' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
'wp_cron' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ),
'language' => get_locale(),
'external_object_cache' => wp_using_ext_object_cache(),
'server_info' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wc_clean( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '',
'php_version' => phpversion(),
'php_post_max_size' => wc_let_to_num( ini_get( 'post_max_size' ) ),
'php_max_execution_time' => (int) ini_get( 'max_execution_time' ),
'php_max_input_vars' => (int) ini_get( 'max_input_vars' ),
'curl_version' => $curl_version,
'suhosin_installed' => extension_loaded( 'suhosin' ),
'max_upload_size' => wp_max_upload_size(),
'mysql_version' => $database_version['number'],
'mysql_version_string' => $database_version['string'],
'default_timezone' => date_default_timezone_get(),
'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ),
'soapclient_enabled' => class_exists( 'SoapClient' ),
'domdocument_enabled' => class_exists( 'DOMDocument' ),
'gzip_enabled' => is_callable( 'gzopen' ),
'mbstring_enabled' => extension_loaded( 'mbstring' ),
'remote_post_successful' => $post_response_successful,
'remote_post_response' => is_wp_error( $post_response_code ) ? $post_response_code->get_error_message() : $post_response_code,
'remote_get_successful' => $get_response_successful,
'remote_get_response' => is_wp_error( $get_response_code ) ? $get_response_code->get_error_message() : $get_response_code,
);
}
/**
* Add prefix to table.
*
* @param string $table Table name.
* @return stromg
*/
protected function add_db_table_prefix( $table ) {
global $wpdb;
return $wpdb->prefix . $table;
}
/**
* Get array of database information. Version, prefix, and table existence.
*
* @return array
*/
public function get_database_info() {
global $wpdb;
$tables = array();
$database_size = array();
// It is not possible to get the database name from some classes that replace wpdb (e.g., HyperDB)
// and that is why this if condition is needed.
if ( defined( 'DB_NAME' ) ) {
$database_table_information = $wpdb->get_results(
$wpdb->prepare(
"SELECT
table_name AS 'name',
engine AS 'engine',
round( ( data_length / 1024 / 1024 ), 2 ) 'data',
round( ( index_length / 1024 / 1024 ), 2 ) 'index'
FROM information_schema.TABLES
WHERE table_schema = %s
ORDER BY name ASC;",
DB_NAME
)
);
// WC Core tables to check existence of.
$core_tables = apply_filters(
'woocommerce_database_tables',
array(
'woocommerce_sessions',
'woocommerce_api_keys',
'woocommerce_attribute_taxonomies',
'woocommerce_downloadable_product_permissions',
'woocommerce_order_items',
'woocommerce_order_itemmeta',
'woocommerce_tax_rates',
'woocommerce_tax_rate_locations',
'woocommerce_shipping_zones',
'woocommerce_shipping_zone_locations',
'woocommerce_shipping_zone_methods',
'woocommerce_payment_tokens',
'woocommerce_payment_tokenmeta',
'woocommerce_log',
)
);
/**
* Adding the prefix to the tables array, for backwards compatibility.
*
* If we changed the tables above to include the prefix, then any filters against that table could break.
*/
$core_tables = array_map( array( $this, 'add_db_table_prefix' ), $core_tables );
/**
* Organize WooCommerce and non-WooCommerce tables separately for display purposes later.
*
* To ensure we include all WC tables, even if they do not exist, pre-populate the WC array with all the tables.
*/
$tables = array(
'woocommerce' => array_fill_keys( $core_tables, false ),
'other' => array(),
);
$database_size = array(
'data' => 0,
'index' => 0,
);
$site_tables_prefix = $wpdb->get_blog_prefix( get_current_blog_id() );
$global_tables = $wpdb->tables( 'global', true );
foreach ( $database_table_information as $table ) {
// Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current.
if ( is_multisite() && 0 !== strpos( $table->name, $site_tables_prefix ) && ! in_array( $table->name, $global_tables, true ) ) {
continue;
}
$table_type = in_array( $table->name, $core_tables, true ) ? 'woocommerce' : 'other';
$tables[ $table_type ][ $table->name ] = array(
'data' => $table->data,
'index' => $table->index,
'engine' => $table->engine,
);
$database_size['data'] += $table->data;
$database_size['index'] += $table->index;
}
}
// Return all database info. Described by JSON Schema.
return array(
'wc_database_version' => get_option( 'woocommerce_db_version' ),
'database_prefix' => $wpdb->prefix,
'maxmind_geoip_database' => '',
'database_tables' => $tables,
'database_size' => $database_size,
);
}
/**
* Get array of counts of objects. Orders, products, etc.
*
* @return array
*/
public function get_post_type_counts() {
global $wpdb;
$post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type;" );
return is_array( $post_type_counts ) ? $post_type_counts : array();
}
/**
* Get a list of plugins active on the site.
*
* @return array
*/
public function get_active_plugins() {
$active_plugins_data = get_transient( 'wc_system_status_active_plugins' );
if ( false === $active_plugins_data ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( ! function_exists( 'get_plugin_data' ) ) {
return array();
}
$active_plugins = (array) get_option( 'active_plugins', array() );
if ( is_multisite() ) {
$network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
$active_plugins = array_merge( $active_plugins, $network_activated_plugins );
}
$active_plugins_data = array();
foreach ( $active_plugins as $plugin ) {
$data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
$active_plugins_data[] = $this->format_plugin_data( $plugin, $data );
}
set_transient( 'wc_system_status_active_plugins', $active_plugins_data, HOUR_IN_SECONDS );
}
return $active_plugins_data;
}
/**
* Get a list of inplugins active on the site.
*
* @return array
*/
public function get_inactive_plugins() {
$plugins_data = get_transient( 'wc_system_status_inactive_plugins' );
if ( false === $plugins_data ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
if ( ! function_exists( 'get_plugins' ) ) {
return array();
}
$plugins = get_plugins();
$active_plugins = (array) get_option( 'active_plugins', array() );
if ( is_multisite() ) {
$network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) );
$active_plugins = array_merge( $active_plugins, $network_activated_plugins );
}
$plugins_data = array();
foreach ( $plugins as $plugin => $data ) {
if ( in_array( $plugin, $active_plugins, true ) ) {
continue;
}
$plugins_data[] = $this->format_plugin_data( $plugin, $data );
}
set_transient( 'wc_system_status_inactive_plugins', $plugins_data, HOUR_IN_SECONDS );
}
return $plugins_data;
}
/**
* Format plugin data, including data on updates, into a standard format.
*
* @since 3.6.0
* @param string $plugin Plugin directory/file.
* @param array $data Plugin data from WP.
* @return array Formatted data.
*/
protected function format_plugin_data( $plugin, $data ) {
require_once ABSPATH . 'wp-admin/includes/update.php';
if ( ! function_exists( 'get_plugin_updates' ) ) {
return array();
}
// Use WP API to lookup latest updates for plugins. WC_Helper injects updates for premium plugins.
if ( empty( $this->available_updates ) ) {
$this->available_updates = get_plugin_updates();
}
$version_latest = $data['Version'];
// Find latest version.
if ( isset( $this->available_updates[ $plugin ]->update->new_version ) ) {
$version_latest = $this->available_updates[ $plugin ]->update->new_version;
}
return array(
'plugin' => $plugin,
'name' => $data['Name'],
'version' => $data['Version'],
'version_latest' => $version_latest,
'url' => $data['PluginURI'],
'author_name' => $data['AuthorName'],
'author_url' => esc_url_raw( $data['AuthorURI'] ),
'network_activated' => $data['Network'],
);
}
/**
* Get a list of Dropins and MU plugins.
*
* @since 3.6.0
* @return array
*/
public function get_dropins_mu_plugins() {
$plugins = get_transient( 'wc_system_status_dropins_mu_plugins' );
if ( false === $plugins ) {
$dropins = get_dropins();
$plugins = array(
'dropins' => array(),
'mu_plugins' => array(),
);
foreach ( $dropins as $key => $dropin ) {
$plugins['dropins'][] = array(
'plugin' => $key,
'name' => $dropin['Name'],
);
}
$mu_plugins = get_mu_plugins();
foreach ( $mu_plugins as $plugin => $mu_plugin ) {
$plugins['mu_plugins'][] = array(
'plugin' => $plugin,
'name' => $mu_plugin['Name'],
'version' => $mu_plugin['Version'],
'url' => $mu_plugin['PluginURI'],
'author_name' => $mu_plugin['AuthorName'],
'author_url' => esc_url_raw( $mu_plugin['AuthorURI'] ),
);
}
set_transient( 'wc_system_status_dropins_mu_plugins', $plugins, HOUR_IN_SECONDS );
}
return $plugins;
}
/**
* Get info on the current active theme, info on parent theme (if presnet)
* and a list of template overrides.
*
* @return array
*/
public function get_theme_info() {
$theme_info = get_transient( 'wc_system_status_theme_info' );
if ( false === $theme_info ) {
$active_theme = wp_get_theme();
// Get parent theme info if this theme is a child theme, otherwise
// pass empty info in the response.
if ( is_child_theme() ) {
$parent_theme = wp_get_theme( $active_theme->template );
$parent_theme_info = array(
'parent_name' => $parent_theme->name,
'parent_version' => $parent_theme->version,
'parent_version_latest' => WC_Admin_Status::get_latest_theme_version( $parent_theme ),
'parent_author_url' => $parent_theme->{'Author URI'},
);
} else {
$parent_theme_info = array(
'parent_name' => '',
'parent_version' => '',
'parent_version_latest' => '',
'parent_author_url' => '',
);
}
/**
* Scan the theme directory for all WC templates to see if our theme
* overrides any of them.
*/
$override_files = array();
$outdated_templates = false;
$scan_files = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates/' );
// Include *-product_<cat|tag> templates for backwards compatibility.
$scan_files[] = 'content-product_cat.php';
$scan_files[] = 'taxonomy-product_cat.php';
$scan_files[] = 'taxonomy-product_tag.php';
foreach ( $scan_files as $file ) {
$located = apply_filters( 'wc_get_template', $file, $file, array(), WC()->template_path(), WC()->plugin_path() . '/templates/' );
if ( file_exists( $located ) ) {
$theme_file = $located;
} elseif ( file_exists( get_stylesheet_directory() . '/' . $file ) ) {
$theme_file = get_stylesheet_directory() . '/' . $file;
} elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) {
$theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file;
} elseif ( file_exists( get_template_directory() . '/' . $file ) ) {
$theme_file = get_template_directory() . '/' . $file;
} elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) {
$theme_file = get_template_directory() . '/' . WC()->template_path() . $file;
} else {
$theme_file = false;
}
if ( ! empty( $theme_file ) ) {
$core_file = $file;
// Update *-product_<cat|tag> template name before searching in core.
if ( false !== strpos( $core_file, '-product_cat' ) || false !== strpos( $core_file, '-product_tag' ) ) {
$core_file = str_replace( '_', '-', $core_file );
}
$core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $core_file );
$theme_version = WC_Admin_Status::get_file_version( $theme_file );
if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) {
if ( ! $outdated_templates ) {
$outdated_templates = true;
}
}
$override_files[] = array(
'file' => str_replace( WP_CONTENT_DIR . '/themes/', '', $theme_file ),
'version' => $theme_version,
'core_version' => $core_version,
);
}
}
$active_theme_info = array(
'name' => $active_theme->name,
'version' => $active_theme->version,
'version_latest' => WC_Admin_Status::get_latest_theme_version( $active_theme ),
'author_url' => esc_url_raw( $active_theme->{'Author URI'} ),
'is_child_theme' => is_child_theme(),
'has_woocommerce_support' => current_theme_supports( 'woocommerce' ),
'has_woocommerce_file' => ( file_exists( get_stylesheet_directory() . '/woocommerce.php' ) || file_exists( get_template_directory() . '/woocommerce.php' ) ),
'has_outdated_templates' => $outdated_templates,
'overrides' => $override_files,
);
$theme_info = array_merge( $active_theme_info, $parent_theme_info );
set_transient( 'wc_system_status_theme_info', $theme_info, HOUR_IN_SECONDS );
}
return $theme_info;
}
/**
* Clear the system status theme cache
*/
public static function clean_theme_cache() {
delete_transient( 'wc_system_status_theme_info' );
}
/**
* Clear the system status plugin caches
*/
public static function clean_plugin_cache() {
delete_transient( 'wc_system_status_active_plugins' );
delete_transient( 'wc_system_status_inactive_plugins' );
delete_transient( 'wc_system_status_dropins_mu_plugins' );
}
/**
* Get some setting values for the site that are useful for debugging
* purposes. For full settings access, use the settings api.
*
* @return array
*/
public function get_settings() {
// Get a list of terms used for product/order taxonomies.
$term_response = array();
$terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
foreach ( $terms as $term ) {
$term_response[ $term->slug ] = strtolower( $term->name );
}
// Get a list of terms used for product visibility.
$product_visibility_terms = array();
$terms = get_terms( 'product_visibility', array( 'hide_empty' => 0 ) );
foreach ( $terms as $term ) {
$product_visibility_terms[ $term->slug ] = strtolower( $term->name );
}
// Return array of useful settings for debugging.
return array(
'api_enabled' => 'yes' === get_option( 'woocommerce_api_enabled' ),
'force_ssl' => 'yes' === get_option( 'woocommerce_force_ssl_checkout' ),
'currency' => get_woocommerce_currency(),
'currency_symbol' => get_woocommerce_currency_symbol(),
'currency_position' => get_option( 'woocommerce_currency_pos' ),
'thousand_separator' => wc_get_price_thousand_separator(),
'decimal_separator' => wc_get_price_decimal_separator(),
'number_of_decimals' => wc_get_price_decimals(),
'geolocation_enabled' => in_array(
get_option( 'woocommerce_default_customer_address' ),
array(
'geolocation_ajax',
'geolocation',
),
true
),
'taxonomies' => $term_response,
'product_visibility_terms' => $product_visibility_terms,
'woocommerce_com_connected' => ConnectionHelper::is_connected() ? 'yes' : 'no',
'enforce_approved_download_dirs' => wc_get_container()->get( Download_Directories::class )->get_mode() === Download_Directories::MODE_ENABLED,
'order_datastore' => WC_Data_Store::load( 'order' )->get_current_class_name(),
'HPOS_feature_screen_enabled' => wc_get_container()->get( Automattic\WooCommerce\Internal\Features\FeaturesController::class )->feature_is_enabled( 'custom_order_tables' ),
'HPOS_enabled' => OrderUtil::custom_orders_table_usage_is_enabled(),
'HPOS_sync_enabled' => wc_get_container()->get( Order_DataSynchronizer::class )->data_sync_is_enabled(),
);
}
/**
* Returns security tips.
*
* @return array
*/
public function get_security_info() {
$check_page = wc_get_page_permalink( 'shop' );
return array(
'secure_connection' => 'https' === substr( $check_page, 0, 5 ),
'hide_errors' => ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ),
);
}
/**
* Returns a mini-report on WC pages and if they are configured correctly:
* Present, visible, and including the correct shortcode or block.
*
* @return array
*/
public function get_pages() {
// WC pages to check against.
$check_pages = array(
_x( 'Shop base', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_shop_page_id',
'shortcode' => '',
'block' => '',
),
_x( 'Cart', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_cart_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']',
'block' => 'woocommerce/cart',
),
_x( 'Checkout', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_checkout_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']',
'block' => 'woocommerce/checkout',
),
_x( 'My account', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_myaccount_page_id',
'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']',
'block' => '',
),
_x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array(
'option' => 'woocommerce_terms_page_id',
'shortcode' => '',
'block' => '',
),
);
$pages_output = array();
foreach ( $check_pages as $page_name => $values ) {
$page_id = get_option( $values['option'] );
$page_set = false;
$page_exists = false;
$page_visible = false;
$shortcode_present = false;
$shortcode_required = false;
$block_present = false;
$block_required = false;
// Page checks.
if ( $page_id ) {
$page_set = true;
}
if ( get_post( $page_id ) ) {
$page_exists = true;
}
if ( 'publish' === get_post_status( $page_id ) ) {
$page_visible = true;
}
// Shortcode checks.
if ( $values['shortcode'] && get_post( $page_id ) ) {
$shortcode_required = true;
$page = get_post( $page_id );
if ( strstr( $page->post_content, $values['shortcode'] ) ) {
$shortcode_present = true;
}
}
// Block checks.
if ( $values['block'] && get_post( $page_id ) ) {
$block_required = true;
$block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] );
}
// Wrap up our findings into an output array.
$pages_output[] = array(
'page_name' => $page_name,
'page_id' => $page_id,
'page_set' => $page_set,
'page_exists' => $page_exists,
'page_visible' => $page_visible,
'shortcode' => $values['shortcode'],
'block' => $values['block'],
'shortcode_required' => $shortcode_required,
'shortcode_present' => $shortcode_present,
'block_present' => $block_present,
'block_required' => $block_required,
);
}
return $pages_output;
}
/**
* Get any query params needed.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
/**
* Prepare the system status response
*
* @param array $system_status System status data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $system_status, $request ) {
$data = $this->add_additional_fields_to_object( $system_status, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
/**
* Filter the system status returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param mixed $system_status System status
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_system_status', $response, $system_status, $request );
}
}
Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php 0000644 00000005656 15154275246 0020743 0 ustar 00 <?php
/**
* REST API Tax Classes controller
*
* Handles requests to the /taxes/classes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Tax Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Tax_Classes_V1_Controller
*/
class WC_REST_Tax_Classes_V2_Controller extends WC_REST_Tax_Classes_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Register the routes for tax classes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<slug>\w[\w\s\-]*)',
array(
'args' => array(
'slug' => array(
'description' => __( 'Unique slug for the resource.', 'woocommerce' ),
'type' => 'string',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get one tax class.
*
* @param WP_REST_Request $request Request object.
* @return array
*/
public function get_item( $request ) {
if ( 'standard' === $request['slug'] ) {
$tax_class = array(
'slug' => 'standard',
'name' => __( 'Standard rate', 'woocommerce' ),
);
} else {
$tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) );
}
$data = array();
if ( $tax_class ) {
$class = $this->prepare_item_for_response( $tax_class, $request );
$class = $this->prepare_response_for_collection( $class );
$data[] = $class;
}
return rest_ensure_response( $data );
}
}
Controllers/Version2/class-wc-rest-taxes-v2-controller.php 0000644 00000000710 15154275246 0017622 0 ustar 00 <?php
/**
* REST API Taxes controller
*
* Handles requests to the /taxes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Taxes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Taxes_V1_Controller
*/
class WC_REST_Taxes_V2_Controller extends WC_REST_Taxes_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
}
Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php 0000644 00000011552 15154275246 0022273 0 ustar 00 <?php
/**
* REST API Webhooks controller
*
* Handles requests to the /webhooks/<webhook_id>/deliveries endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Webhook Deliveries controller class.
*
* @deprecated 3.3.0 Webhooks deliveries logs now uses logging system.
* @package WooCommerce\RestApi
* @extends WC_REST_Webhook_Deliveries_V1_Controller
*/
class WC_REST_Webhook_Deliveries_V2_Controller extends WC_REST_Webhook_Deliveries_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Prepare a single webhook delivery output for response.
*
* @param stdClass $log Delivery log object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $log, $request ) {
$data = (array) $log;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $log ) );
/**
* Filter webhook delivery object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param stdClass $log Delivery log object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request );
}
/**
* Get the Webhook's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'webhook_delivery',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view' ),
'readonly' => true,
),
'duration' => array(
'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'summary' => array(
'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'request_url' => array(
'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view' ),
'readonly' => true,
),
'request_headers' => array(
'description' => __( 'Request headers.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'request_body' => array(
'description' => __( 'Request body.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_code' => array(
'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_message' => array(
'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'response_headers' => array(
'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'response_body' => array(
'description' => __( 'The response body from the receiving server.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the webhook delivery was logged, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version2/class-wc-rest-webhooks-v2-controller.php 0000644 00000014216 15154275246 0020325 0 ustar 00 <?php
/**
* REST API Webhooks controller
*
* Handles requests to the /webhooks endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Webhooks controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Webhooks_V1_Controller
*/
class WC_REST_Webhooks_V2_Controller extends WC_REST_Webhooks_V1_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Prepare a single webhook output for response.
*
* @param int $id Webhook ID.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response
*/
public function prepare_item_for_response( $id, $request ) {
$webhook = wc_get_webhook( $id );
if ( empty( $webhook ) || is_null( $webhook ) ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
$data = array(
'id' => $webhook->get_id(),
'name' => $webhook->get_name(),
'status' => $webhook->get_status(),
'topic' => $webhook->get_topic(),
'resource' => $webhook->get_resource(),
'event' => $webhook->get_event(),
'hooks' => $webhook->get_hooks(),
'delivery_url' => $webhook->get_delivery_url(),
'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created(), false ),
'date_created_gmt' => wc_rest_prepare_date_response( $webhook->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified(), false ),
'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_date_modified() ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $webhook->get_id(), $request ) );
/**
* Filter webhook object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WC_Webhook $webhook Webhook object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request );
}
/**
* Get the default REST API version.
*
* @since 3.0.0
* @return string
*/
protected function get_default_api_version() {
return 'wp_api_v2';
}
/**
* Get the Webhook's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'webhook',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'A friendly name for the webhook.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Webhook status.', 'woocommerce' ),
'type' => 'string',
'default' => 'active',
'enum' => array_keys( wc_get_webhook_statuses() ),
'context' => array( 'view', 'edit' ),
),
'topic' => array(
'description' => __( 'Webhook topic.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'resource' => array(
'description' => __( 'Webhook resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'event' => array(
'description' => __( 'Webhook event.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'hooks' => array(
'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'delivery_url' => array(
'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'secret' => array(
'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'date_created' => array(
'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the webhook was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the webhook was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-controller.php 0000644 00000043155 15154275246 0016166 0 ustar 00 <?php
/**
* REST Controller
*
* This class extend `WP_REST_Controller` in order to include /batch endpoint
* for almost all endpoints in WooCommerce REST API.
*
* It's required to follow "Controller Classes" guide before extending this class:
* <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/>
*
* NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS.
* If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`.
*
* @class WC_REST_Controller
* @package WooCommerce\RestApi
* @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Abstract Rest Controller Class
*
* @package WooCommerce\RestApi
* @extends WP_REST_Controller
* @version 2.6.0
*/
abstract class WC_REST_Controller extends WP_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = '';
/**
* Used to cache computed return fields.
*
* @var null|array
*/
private $_fields = null;
/**
* Used to verify if cached fields are for correct request object.
*
* @var null|WP_REST_Request
*/
private $_request = null;
/**
* Add the schema from additional fields to an schema array.
*
* The type of object is inferred from the passed schema.
*
* @param array $schema Schema array.
*
* @return array
*/
protected function add_additional_fields_schema( $schema ) {
if ( empty( $schema['title'] ) ) {
return $schema;
}
/**
* Can't use $this->get_object_type otherwise we cause an inf loop.
*/
$object_type = $schema['title'];
$additional_fields = $this->get_additional_fields( $object_type );
foreach ( $additional_fields as $field_name => $field_options ) {
if ( ! $field_options['schema'] ) {
continue;
}
$schema['properties'][ $field_name ] = $field_options['schema'];
}
$schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] );
return $schema;
}
/**
* Compatibility functions for WP 5.5, since custom types are not supported anymore.
* See @link https://core.trac.wordpress.org/changeset/48306
*
* @param string $method Optional. HTTP method of the request.
*
* @return array Endpoint arguments.
*/
public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
$endpoint_args = parent::get_endpoint_args_for_item_schema( $method );
if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) {
return $endpoint_args;
}
$endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args );
return $endpoint_args;
}
/**
* Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore.
*
* See @link https://core.trac.wordpress.org/changeset/48306
*
* We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema.
*
* @param array $endpoint_args Schema with datatypes to convert.
* @return mixed Schema with converted datatype.
*/
protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) {
if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) {
return $endpoint_args;
}
foreach ( $endpoint_args as $field_id => $params ) {
if ( ! isset( $params['type'] ) ) {
continue;
}
/**
* Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'.
*/
if ( 'date-time' === $params['type'] ) {
$params['type'] = array( 'null', 'string' );
}
/**
* WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods.
*/
if ( 'mixed' === $params['type'] ) {
$params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' );
}
if ( isset( $params['properties'] ) ) {
$params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] );
}
if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) {
$params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] );
}
$endpoint_args[ $field_id ] = $params;
}
return $endpoint_args;
}
/**
* Get normalized rest base.
*
* @return string
*/
protected function get_normalized_rest_base() {
return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
}
/**
* Check batch limit.
*
* @param array $items Request items.
* @return bool|WP_Error
*/
protected function check_batch_limit( $items ) {
$limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
$total = 0;
if ( ! empty( $items['create'] ) ) {
$total += count( $items['create'] );
}
if ( ! empty( $items['update'] ) ) {
$total += count( $items['update'] );
}
if ( ! empty( $items['delete'] ) ) {
$total += count( $items['delete'] );
}
if ( $total > $limit ) {
/* translators: %s: items limit */
return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) );
}
return true;
}
/**
* Bulk create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Of WP_Error or WP_REST_Response.
*/
public function batch_items( $request ) {
/**
* REST Server
*
* @var WP_REST_Server $wp_rest_server
*/
global $wp_rest_server;
// Get the request params.
$items = array_filter( $request->get_params() );
$query = $request->get_query_params();
$response = array();
// Check batch limit.
$limit = $this->check_batch_limit( $items );
if ( is_wp_error( $limit ) ) {
return $limit;
}
if ( ! empty( $items['create'] ) ) {
foreach ( $items['create'] as $item ) {
$_item = new WP_REST_Request( 'POST', $request->get_route() );
// Default parameters.
$defaults = array();
$schema = $this->get_public_item_schema();
foreach ( $schema['properties'] as $arg => $options ) {
if ( isset( $options['default'] ) ) {
$defaults[ $arg ] = $options['default'];
}
}
$_item->set_default_params( $defaults );
// Set request parameters.
$_item->set_body_params( $item );
// Set query (GET) parameters.
$_item->set_query_params( $query );
$_response = $this->create_item( $_item );
if ( is_wp_error( $_response ) ) {
$response['create'][] = array(
'id' => 0,
'error' => array(
'code' => $_response->get_error_code(),
'message' => $_response->get_error_message(),
'data' => $_response->get_error_data(),
),
);
} else {
$response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
}
}
}
if ( ! empty( $items['update'] ) ) {
foreach ( $items['update'] as $item ) {
$_item = new WP_REST_Request( 'PUT', $request->get_route() );
$_item->set_body_params( $item );
$_response = $this->update_item( $_item );
if ( is_wp_error( $_response ) ) {
$response['update'][] = array(
'id' => $item['id'],
'error' => array(
'code' => $_response->get_error_code(),
'message' => $_response->get_error_message(),
'data' => $_response->get_error_data(),
),
);
} else {
$response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
}
}
}
if ( ! empty( $items['delete'] ) ) {
foreach ( $items['delete'] as $id ) {
$id = (int) $id;
if ( 0 === $id ) {
continue;
}
$_item = new WP_REST_Request( 'DELETE', $request->get_route() );
$_item->set_query_params(
array(
'id' => $id,
'force' => true,
)
);
$_response = $this->delete_item( $_item );
if ( is_wp_error( $_response ) ) {
$response['delete'][] = array(
'id' => $id,
'error' => array(
'code' => $_response->get_error_code(),
'message' => $_response->get_error_message(),
'data' => $_response->get_error_data(),
),
);
} else {
$response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
}
}
}
return $response;
}
/**
* Validate a text value for a text based setting.
*
* @since 3.0.0
* @param string $value Value.
* @param array $setting Setting.
* @return string
*/
public function validate_setting_text_field( $value, $setting ) {
$value = is_null( $value ) ? '' : $value;
return wp_kses_post( trim( stripslashes( $value ) ) );
}
/**
* Validate select based settings.
*
* @since 3.0.0
* @param string $value Value.
* @param array $setting Setting.
* @return string|WP_Error
*/
public function validate_setting_select_field( $value, $setting ) {
if ( array_key_exists( $value, $setting['options'] ) ) {
return $value;
} else {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
/**
* Validate multiselect based settings.
*
* @since 3.0.0
* @param array $values Values.
* @param array $setting Setting.
* @return array|WP_Error
*/
public function validate_setting_multiselect_field( $values, $setting ) {
if ( empty( $values ) ) {
return array();
}
if ( ! is_array( $values ) ) {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
$final_values = array();
foreach ( $values as $value ) {
if ( array_key_exists( $value, $setting['options'] ) ) {
$final_values[] = $value;
}
}
return $final_values;
}
/**
* Validate image_width based settings.
*
* @since 3.0.0
* @param array $values Values.
* @param array $setting Setting.
* @return string|WP_Error
*/
public function validate_setting_image_width_field( $values, $setting ) {
if ( ! is_array( $values ) ) {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
$current = $setting['value'];
if ( isset( $values['width'] ) ) {
$current['width'] = intval( $values['width'] );
}
if ( isset( $values['height'] ) ) {
$current['height'] = intval( $values['height'] );
}
if ( isset( $values['crop'] ) ) {
$current['crop'] = (bool) $values['crop'];
}
return $current;
}
/**
* Validate radio based settings.
*
* @since 3.0.0
* @param string $value Value.
* @param array $setting Setting.
* @return string|WP_Error
*/
public function validate_setting_radio_field( $value, $setting ) {
return $this->validate_setting_select_field( $value, $setting );
}
/**
* Validate checkbox based settings.
*
* @since 3.0.0
* @param string $value Value.
* @param array $setting Setting.
* @return string|WP_Error
*/
public function validate_setting_checkbox_field( $value, $setting ) {
if ( in_array( $value, array( 'yes', 'no' ) ) ) {
return $value;
} elseif ( empty( $value ) ) {
$value = isset( $setting['default'] ) ? $setting['default'] : 'no';
return $value;
} else {
return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) );
}
}
/**
* Validate textarea based settings.
*
* @since 3.0.0
* @param string $value Value.
* @param array $setting Setting.
* @return string
*/
public function validate_setting_textarea_field( $value, $setting ) {
$value = is_null( $value ) ? '' : $value;
return wp_kses(
trim( stripslashes( $value ) ),
array_merge(
array(
'iframe' => array(
'src' => true,
'style' => true,
'id' => true,
'class' => true,
),
),
wp_kses_allowed_html( 'post' )
)
);
}
/**
* Add meta query.
*
* @since 3.0.0
* @param array $args Query args.
* @param array $meta_query Meta query.
* @return array
*/
protected function add_meta_query( $args, $meta_query ) {
if ( empty( $args['meta_query'] ) ) {
$args['meta_query'] = array();
}
$args['meta_query'][] = $meta_query;
return $args['meta_query'];
}
/**
* Get the batch schema, conforming to JSON Schema.
*
* @return array
*/
public function get_public_batch_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'batch',
'type' => 'object',
'properties' => array(
'create' => array(
'description' => __( 'List of created resources.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
),
),
'update' => array(
'description' => __( 'List of updated resources.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
),
),
'delete' => array(
'description' => __( 'List of delete resources.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'integer',
),
),
),
);
return $schema;
}
/**
* Gets an array of fields to be included on the response.
*
* Included fields are based on item schema and `_fields=` request argument.
* Updated from WordPress 5.3, included into this class to support old versions.
*
* @since 3.5.0
* @param WP_REST_Request $request Full details about the request.
* @return array Fields to be included in the response.
*/
public function get_fields_for_response( $request ) {
// From xdebug profiling, this method could take upto 25% of request time in index calls.
// Cache it and make sure _fields was cached on current request object!
// TODO: Submit this caching behavior in core.
if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) {
return $this->_fields;
}
$this->_request = $request;
$schema = $this->get_item_schema();
$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
$additional_fields = $this->get_additional_fields();
foreach ( $additional_fields as $field_name => $field_options ) {
// For back-compat, include any field with an empty schema
// because it won't be present in $this->get_item_schema().
if ( is_null( $field_options['schema'] ) ) {
$properties[ $field_name ] = $field_options;
}
}
// Exclude fields that specify a different context than the request context.
$context = $request['context'];
if ( $context ) {
foreach ( $properties as $name => $options ) {
if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
unset( $properties[ $name ] );
}
}
}
$fields = array_keys( $properties );
if ( ! isset( $request['_fields'] ) ) {
$this->_fields = $fields;
return $fields;
}
$requested_fields = wp_parse_list( $request['_fields'] );
if ( 0 === count( $requested_fields ) ) {
$this->_fields = $fields;
return $fields;
}
// Trim off outside whitespace from the comma delimited list.
$requested_fields = array_map( 'trim', $requested_fields );
// Always persist 'id', because it can be needed for add_additional_fields_to_object().
if ( in_array( 'id', $fields, true ) ) {
$requested_fields[] = 'id';
}
// Return the list of all requested fields which appear in the schema.
$this->_fields = array_reduce(
$requested_fields,
function( $response_fields, $field ) use ( $fields ) {
if ( in_array( $field, $fields, true ) ) {
$response_fields[] = $field;
return $response_fields;
}
// Check for nested fields if $field is not a direct match.
$nested_fields = explode( '.', $field );
// A nested field is included so long as its top-level property
// is present in the schema.
if ( in_array( $nested_fields[0], $fields, true ) ) {
$response_fields[] = $field;
}
return $response_fields;
},
array()
);
return $this->_fields;
}
/**
* Limit the contents of the meta_data property based on certain request parameters.
*
* Note that if both `include_meta` and `exclude_meta` are present in the request,
* `include_meta` will take precedence.
*
* @param \WP_REST_Request $request The request.
* @param array $meta_data All of the meta data for an object.
*
* @return array
*/
protected function get_meta_data_for_response( $request, $meta_data ) {
$fields = $this->get_fields_for_response( $request );
if ( ! in_array( 'meta_data', $fields, true ) ) {
return array();
}
$include = (array) $request['include_meta'];
$exclude = (array) $request['exclude_meta'];
if ( ! empty( $include ) ) {
$meta_data = array_filter(
$meta_data,
function( WC_Meta_Data $item ) use ( $include ) {
$data = $item->get_data();
return in_array( $data['key'], $include, true );
}
);
} elseif ( ! empty( $exclude ) ) {
$meta_data = array_filter(
$meta_data,
function( WC_Meta_Data $item ) use ( $exclude ) {
$data = $item->get_data();
return ! in_array( $data['key'], $exclude, true );
}
);
}
// Ensure the array indexes are reset so it doesn't get converted to an object in JSON.
return array_values( $meta_data );
}
}
Controllers/Version3/class-wc-rest-coupons-controller.php 0000644 00000001734 15154275246 0017647 0 ustar 00 <?php
/**
* REST API Coupons controller
*
* Handles requests to the /coupons endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Coupons controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Coupons_V2_Controller
*/
class WC_REST_Coupons_Controller extends WC_REST_Coupons_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Prepare objects query.
*
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = parent::prepare_objects_query( $request );
// Set post_status.
$args['post_status'] = $request['status'];
if ( ! empty( $request['code'] ) ) {
$id = wc_get_coupon_id_by_code( $request['code'] );
$args['post__in'] = array( $id );
}
// Get only ids.
$args['fields'] = 'ids';
return $args;
}
}
Controllers/Version3/class-wc-rest-crud-controller.php 0000644 00000056664 15154275246 0017132 0 ustar 00 <?php
/**
* Abstract Rest CRUD Controller Class
*
* @class WC_REST_CRUD_Controller
* @package WooCommerce\RestApi
* @version 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_REST_CRUD_Controller class.
*
* @extends WC_REST_Posts_Controller
*/
abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* If object is hierarchical.
*
* @var bool
*/
protected $hierarchical = false;
/**
* Get object.
*
* @param int $id Object ID.
* @return object WC_Data object or WP_Error object.
*/
protected function get_object( $id ) {
// translators: %s: Class method name.
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Check if a given request has access to read an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get object permalink.
*
* @param object $object Object.
* @return string
*/
protected function get_permalink( $object ) {
return '';
}
/**
* Prepares the object for the REST response.
*
* @since 3.0.0
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
protected function prepare_object_for_response( $object, $request ) {
// translators: %s: Class method name.
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Prepares one object for create or update operation.
*
* @since 3.0.0
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
*/
protected function prepare_object_for_database( $request, $creating = false ) {
// translators: %s: Class method name.
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
}
/**
* Get a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = $this->prepare_object_for_response( $object, $request );
$response = rest_ensure_response( $data );
if ( $this->public ) {
$response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) );
}
return $response;
}
/**
* Save an object data.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @param bool $creating If is creating a new object.
* @return WC_Data|WP_Error
*/
protected function save_object( $request, $creating = false ) {
try {
$object = $this->prepare_object_for_database( $request, $creating );
if ( is_wp_error( $object ) ) {
return $object;
}
$object->save();
return $this->get_object( $object->get_id() );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Create a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$object = $this->save_object( $request, true );
if ( is_wp_error( $object ) ) {
return $object;
}
try {
$this->update_additional_fields_for_object( $object, $request );
/**
* Fires after a single object is created or updated via the REST API.
*
* @param WC_Data $object Inserted object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating object, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true );
} catch ( WC_Data_Exception $e ) {
$object->delete();
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
$object->delete();
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_object_for_response( $object, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) );
return $response;
}
/**
* Update a single post.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$object = $this->get_object( (int) $request['id'] );
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) );
}
$object = $this->save_object( $request, false );
if ( is_wp_error( $object ) ) {
return $object;
}
try {
$this->update_additional_fields_for_object( $object, $request );
/**
* Fires after a single object is created or updated via the REST API.
*
* @param WC_Data $object Inserted object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating object, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_object_for_response( $object, $request );
return rest_ensure_response( $response );
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = array();
$args['offset'] = $request['offset'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['paged'] = $request['page'];
$args['post__in'] = $request['include'];
$args['post__not_in'] = $request['exclude'];
$args['posts_per_page'] = $request['per_page'];
$args['name'] = $request['slug'];
$args['post_parent__in'] = $request['parent'];
$args['post_parent__not_in'] = $request['parent_exclude'];
$args['s'] = $request['search'];
$args['fields'] = $this->get_fields_for_response( $request );
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';
}
$date_query = array();
$use_gmt = $request['dates_are_gmt'];
if ( isset( $request['before'] ) ) {
$date_query[] = array(
'column' => $use_gmt ? 'post_date_gmt' : 'post_date',
'before' => $request['before'],
);
}
if ( isset( $request['after'] ) ) {
$date_query[] = array(
'column' => $use_gmt ? 'post_date_gmt' : 'post_date',
'after' => $request['after'],
);
}
if ( isset( $request['modified_before'] ) ) {
$date_query[] = array(
'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified',
'before' => $request['modified_before'],
);
}
if ( isset( $request['modified_after'] ) ) {
$date_query[] = array(
'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified',
'after' => $request['modified_after'],
);
}
if ( ! empty( $date_query ) ) {
$date_query['relation'] = 'AND';
$args['date_query'] = $date_query;
}
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
/**
* Filter the query arguments for a request.
*
* Enables adding extra arguments or setting defaults for a post
* collection request.
*
* @param array $args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request );
return $this->prepare_items_query( $args, $request );
}
/**
* Get objects.
*
* @since 3.0.0
* @param array $query_args Query args.
* @return array
*/
protected function get_objects( $query_args ) {
$query = new WP_Query();
$result = $query->query( $query_args );
$total_posts = $query->found_posts;
if ( $total_posts < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
return array(
'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ),
'total' => (int) $total_posts,
'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ),
);
}
/**
* Get a collection of posts.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$query_args = $this->prepare_objects_query( $request );
if ( is_wp_error( current( $query_args ) ) ) {
return current( $query_args );
}
$query_results = $this->get_objects( $query_args );
$objects = array();
foreach ( $query_results['objects'] as $object ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) {
continue;
}
$data = $this->prepare_object_for_response( $object, $request );
$objects[] = $this->prepare_response_for_collection( $data );
}
$page = (int) $query_args['paged'];
$max_pages = $query_results['pages'];
$response = rest_ensure_response( $objects );
$response->header( 'X-WP-Total', $query_results['total'] );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = $this->rest_base;
$attrib_prefix = '(?P<';
if ( strpos( $base, $attrib_prefix ) !== false ) {
$attrib_names = array();
preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE );
foreach ( $attrib_names as $attrib_name_match ) {
$beginning_offset = strlen( $attrib_prefix );
$attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] );
$attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset );
if ( isset( $request[ $attrib_name ] ) ) {
$base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base );
}
}
}
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Delete a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$force = (bool) $request['force'];
$object = $this->get_object( (int) $request['id'] );
$result = false;
if ( ! $object || 0 === $object->get_id() ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) );
/**
* Filter whether an object is trashable.
*
* Return false to disable trash support for the object.
*
* @param boolean $supports_trash Whether the object type support trashing.
* @param WC_Data $object The object being considered for trashing support.
*/
$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_object_for_response( $object, $request );
// If we're forcing, then delete permanently.
if ( $force ) {
$object->delete( true );
$result = 0 === $object->get_id();
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
}
// Otherwise, only trash if we haven't already.
if ( is_callable( array( $object, 'get_status' ) ) ) {
if ( 'trash' === $object->get_status() ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
}
$object->delete();
$result = 'trash' === $object->get_status();
}
}
if ( ! $result ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
}
/**
* Fires after a single object is deleted or trashed via the REST API.
*
* @param WC_Data $object The deleted or trashed object.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request );
return $response;
}
/**
* Get fields for an object if getter is defined.
*
* @param object $object Object we are fetching response for.
* @param string $context Context of the request. Can be `view` or `edit`.
* @param array $fields List of fields to fetch.
* @return array Data fetched from getters.
*/
public function fetch_fields_using_getters( $object, $context, $fields ) {
$data = array();
foreach ( $fields as $field ) {
if ( method_exists( $this, "api_get_$field" ) ) {
$data[ $field ] = $this->{"api_get_$field"}( $object, $context );
}
}
return $data;
}
/**
* Prepare links for the request.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param();
$params['context']['default'] = 'view';
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['search'] = array(
'description' => __( 'Limit results to those matching a string.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['modified_after'] = array(
'description' => __( 'Limit response to resources modified after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['modified_before'] = array(
'description' => __( 'Limit response to resources modified before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['dates_are_gmt'] = array(
'description' => __( 'Whether to consider GMT post dates when limiting response by published or modified date.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'validate_callback' => 'rest_validate_request_arg',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'id',
'include',
'title',
'slug',
'modified',
),
'validate_callback' => 'rest_validate_request_arg',
);
if ( $this->hierarchical ) {
$params['parent'] = array(
'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
}
/**
* Filter collection parameters for the posts controller.
*
* The dynamic part of the filter `$this->post_type` refers to the post
* type slug for the controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Query parameter. Use the
* `rest_{$this->post_type}_query` filter to set WP_Query parameters.
*
* @param array $query_params JSON Schema-formatted collection parameters.
* @param WP_Post_Type $post_type Post type object.
*/
return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type );
}
}
Controllers/Version3/class-wc-rest-customer-downloads-controller.php 0000644 00000001031 15154275246 0022000 0 ustar 00 <?php
/**
* REST API Customer Downloads controller
*
* Handles requests to the /customers/<customer_id>/downloads endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Customer_Downloads_V2_Controller
*/
class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-customers-controller.php 0000644 00000022430 15154275246 0020201 0 ustar 00 <?php
/**
* REST API Customers controller
*
* Handles requests to the /customers endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Customers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Customers_V2_Controller
*/
class WC_REST_Customers_Controller extends WC_REST_Customers_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Get formatted item data.
*
* @param WC_Data $object WC_Data instance.
*
* @since 3.0.0
* @return array
*/
protected function get_formatted_item_data( $object ) {
return $this->get_formatted_item_data_core( $object );
}
/**
* Get the Customer's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'customer',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'email' => array(
'description' => __( 'The email address for the customer.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'first_name' => array(
'description' => __( 'Customer first name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'last_name' => array(
'description' => __( 'Customer last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'role' => array(
'description' => __( 'Customer role.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'username' => array(
'description' => __( 'Customer login name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_user',
),
),
'password' => array(
'description' => __( 'Customer password.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'billing' => array(
'description' => __( 'List of billing address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'email' => array(
'description' => __( 'Email address.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping' => array(
'description' => __( 'List of shipping address data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'first_name' => array(
'description' => __( 'First name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'last_name' => array(
'description' => __( 'Last name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'company' => array(
'description' => __( 'Company name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_1' => array(
'description' => __( 'Address line 1', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'address_2' => array(
'description' => __( 'Address line 2', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'city' => array(
'description' => __( 'City name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'state' => array(
'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'postcode' => array(
'description' => __( 'Postal code.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'country' => array(
'description' => __( 'ISO code of the country.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'phone' => array(
'description' => __( 'Phone number.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'is_paying_customer' => array(
'description' => __( 'Is the customer a paying customer?', 'woocommerce' ),
'type' => 'bool',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avatar_url' => array(
'description' => __( 'Avatar URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-data-continents-controller.php 0000644 00000025530 15154275246 0021254 0 ustar 00 <?php
/**
* REST API Data continents controller.
*
* Handles requests to the /data/continents endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Data continents controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Data_Continents_Controller extends WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data/continents';
/**
* Register routes.
*
* @since 3.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<location>[\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'continent' => array(
'description' => __( '2 character continent code.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Return the list of countries and states for a given continent.
*
* @since 3.5.0
* @param string $continent_code Continent code.
* @param WP_REST_Request $request Request data.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function get_continent( $continent_code, $request ) {
$continents = WC()->countries->get_continents();
$countries = WC()->countries->get_countries();
$states = WC()->countries->get_states();
$locale_info = include WC()->plugin_path() . '/i18n/locale-info.php';
$data = array();
if ( ! array_key_exists( $continent_code, $continents ) ) {
return false;
}
$continent_list = $continents[ $continent_code ];
$continent = array(
'code' => $continent_code,
'name' => $continent_list['name'],
);
$local_countries = array();
foreach ( $continent_list['countries'] as $country_code ) {
if ( isset( $countries[ $country_code ] ) ) {
$country = array(
'code' => $country_code,
'name' => $countries[ $country_code ],
);
// If we have detailed locale information include that in the response.
if ( array_key_exists( $country_code, $locale_info ) ) {
// Defensive programming against unexpected changes in locale-info.php.
$country_data = wp_parse_args(
$locale_info[ $country_code ],
array(
'currency_code' => 'USD',
'currency_pos' => 'left',
'decimal_sep' => '.',
'dimension_unit' => 'in',
'num_decimals' => 2,
'thousand_sep' => ',',
'weight_unit' => 'lbs',
)
);
$country = array_merge( $country, $country_data );
}
$local_states = array();
if ( isset( $states[ $country_code ] ) ) {
foreach ( $states[ $country_code ] as $state_code => $state_name ) {
$local_states[] = array(
'code' => $state_code,
'name' => $state_name,
);
}
}
$country['states'] = $local_states;
// Allow only desired keys (e.g. filter out tax rates).
$allowed = array(
'code',
'currency_code',
'currency_pos',
'decimal_sep',
'dimension_unit',
'name',
'num_decimals',
'states',
'thousand_sep',
'weight_unit',
);
$country = array_intersect_key( $country, array_flip( $allowed ) );
$local_countries[] = $country;
}
}
$continent['countries'] = $local_countries;
return $continent;
}
/**
* Return the list of states for all continents.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$continents = WC()->countries->get_continents();
$data = array();
foreach ( array_keys( $continents ) as $continent_code ) {
$continent = $this->get_continent( $continent_code, $request );
$response = $this->prepare_item_for_response( $continent, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
return rest_ensure_response( $data );
}
/**
* Return the list of locations for a given continent.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$data = $this->get_continent( strtoupper( $request['location'] ), $request );
if ( empty( $data ) ) {
return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $data, $request );
}
/**
* Prepare the data object for response.
*
* @since 3.5.0
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter the location list returned from the API.
*
* Allows modification of the location data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param array $item The original list of continent(s), countries, and states.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given continent.
*/
protected function prepare_links( $item ) {
$continent_code = strtolower( $item['code'] );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the location schema, conforming to JSON Schema.
*
* @since 3.5.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_continents',
'type' => 'object',
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( '2 character continent code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of continent.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'countries' => array(
'type' => 'array',
'description' => __( 'List of countries on this continent.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'currency_code' => array(
'type' => 'string',
'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'currency_pos' => array(
'type' => 'string',
'description' => __( 'Currency symbol position for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'decimal_sep' => array(
'type' => 'string',
'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'dimension_unit' => array(
'type' => 'string',
'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'num_decimals' => array(
'type' => 'integer',
'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'states' => array(
'type' => 'array',
'description' => __( 'List of states in this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'State code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of state.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
'thousand_sep' => array(
'type' => 'string',
'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'weight_unit' => array(
'type' => 'string',
'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-data-controller.php 0000644 00000011343 15154275246 0017067 0 ustar 00 <?php
/**
* REST API Data controller.
*
* Handles requests to the /data endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Data controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Data_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data';
/**
* Register routes.
*
* @since 3.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Check whether a given request has permission to read site data.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to read site settings.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Return the list of data resources.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$data = array();
$resources = array(
array(
'slug' => 'continents',
'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ),
),
array(
'slug' => 'countries',
'description' => __( 'List of supported states in a given country.', 'woocommerce' ),
),
array(
'slug' => 'currencies',
'description' => __( 'List of supported currencies.', 'woocommerce' ),
),
);
foreach ( $resources as $resource ) {
$item = $this->prepare_item_for_response( (object) $resource, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Prepare a data resource object for serialization.
*
* @param stdClass $resource Resource data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $resource, $request ) {
$data = array(
'slug' => $resource->slug,
'description' => $resource->description,
);
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, 'view' );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $resource ) );
return $response;
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given country.
*/
protected function prepare_links( $item ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the data index schema, conforming to JSON Schema.
*
* @since 3.5.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_index',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'Data resource ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Data resource description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-data-countries-controller.php 0000644 00000015160 15154275246 0021101 0 ustar 00 <?php
/**
* REST API Data countries controller.
*
* Handles requests to the /data/countries endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Data countries controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Data_Countries_Controller extends WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data/countries';
/**
* Register routes.
*
* @since 3.5.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<location>[\w-]+)',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
'location' => array(
'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get a list of countries and states.
*
* @param string $country_code Country code.
* @param WP_REST_Request $request Request data.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function get_country( $country_code, $request ) {
$countries = WC()->countries->get_countries();
$states = WC()->countries->get_states();
$data = array();
if ( ! array_key_exists( $country_code, $countries ) ) {
return false;
}
$country = array(
'code' => $country_code,
'name' => $countries[ $country_code ],
);
$local_states = array();
if ( isset( $states[ $country_code ] ) ) {
foreach ( $states[ $country_code ] as $state_code => $state_name ) {
$local_states[] = array(
'code' => $state_code,
'name' => $state_name,
);
}
}
$country['states'] = $local_states;
return $country;
}
/**
* Return the list of states for all countries.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$countries = WC()->countries->get_countries();
$data = array();
foreach ( array_keys( $countries ) as $country_code ) {
$country = $this->get_country( $country_code, $request );
$response = $this->prepare_item_for_response( $country, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
return rest_ensure_response( $data );
}
/**
* Return the list of states for a given country.
*
* @since 3.5.0
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$data = $this->get_country( strtoupper( $request['location'] ), $request );
if ( empty( $data ) ) {
return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $data, $request );
}
/**
* Prepare the data object for response.
*
* @since 3.5.0
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter the states list for a country returned from the API.
*
* Allows modification of the location data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param array $data The original country's states list.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given country.
*/
protected function prepare_links( $item ) {
$country_code = strtolower( $item['code'] );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the location schema, conforming to JSON Schema.
*
* @since 3.5.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_countries',
'type' => 'object',
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'states' => array(
'type' => 'array',
'description' => __( 'List of states in this country.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'context' => array( 'view' ),
'readonly' => true,
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'State code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of state.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-data-currencies-controller.php 0000644 00000014164 15154275246 0021233 0 ustar 00 <?php
/**
* REST API Data currencies controller.
*
* Handles requests to the /data/currencies endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Data Currencies controller class.
*
* @package WooCommerce\RestApi
*/
class WC_REST_Data_Currencies_Controller extends WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'data/currencies';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/current',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_current_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<currency>[\w-]{3})',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'location' => array(
'description' => __( 'ISO4217 currency code.', 'woocommerce' ),
'type' => 'string',
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Get currency information.
*
* @param string $code Currency code.
* @param WP_REST_Request $request Request data.
* @return array|mixed Response data, ready for insertion into collection data.
*/
public function get_currency( $code, $request ) {
$currencies = get_woocommerce_currencies();
$data = array();
if ( ! array_key_exists( $code, $currencies ) ) {
return false;
}
$currency = array(
'code' => $code,
'name' => $currencies[ $code ],
'symbol' => get_woocommerce_currency_symbol( $code ),
);
return $currency;
}
/**
* Return the list of currencies.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$currencies = get_woocommerce_currencies();
foreach ( array_keys( $currencies ) as $code ) {
$currency = $this->get_currency( $code, $request );
$response = $this->prepare_item_for_response( $currency, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
return rest_ensure_response( $data );
}
/**
* Return information for a specific currency.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$data = $this->get_currency( strtoupper( $request['currency'] ), $request );
if ( empty( $data ) ) {
return new WP_Error( 'woocommerce_rest_data_invalid_currency', __( 'There are no currencies matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $this->prepare_item_for_response( $data, $request );
}
/**
* Return information for the current site currency.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function get_current_item( $request ) {
$currency = get_option( 'woocommerce_currency' );
return $this->prepare_item_for_response( $this->get_currency( $currency, $request ), $request );
}
/**
* Prepare the data object for response.
*
* @param object $item Data object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, $request ) {
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, 'view' );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item ) );
/**
* Filter currency returned from the API.
*
* @param WP_REST_Response $response The response object.
* @param array $item Currency data.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_data_currency', $response, $item, $request );
}
/**
* Prepare links for the request.
*
* @param object $item Data object.
* @return array Links for the given currency.
*/
protected function prepare_links( $item ) {
$code = strtoupper( $item['code'] );
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $code ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Get the currency schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'data_currencies',
'type' => 'object',
'properties' => array(
'code' => array(
'type' => 'string',
'description' => __( 'ISO4217 currency code.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'type' => 'string',
'description' => __( 'Full name of currency.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
'symbol' => array(
'type' => 'string',
'description' => __( 'Currency symbol.', 'woocommerce' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-network-orders-controller.php 0000644 00000000772 15154275246 0021147 0 ustar 00 <?php
/**
* REST API Network Orders controller
*
* Handles requests to the /orders/network endpoint
*
* @package WooCommerce\RestApi
* @since 3.4.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Network Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Network_Orders_V2_Controller
*/
class WC_REST_Network_Orders_Controller extends WC_REST_Network_Orders_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-order-notes-controller.php 0000644 00000013614 15154275246 0020422 0 ustar 00 <?php
/**
* REST API Order Notes controller
*
* Handles requests to the /orders/<order_id>/notes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Order Notes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Order_Notes_V2_Controller
*/
class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Prepare a single order note output for response.
*
* @param WP_Comment $note Order note object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $note, $request ) {
$data = array(
'id' => (int) $note->comment_ID,
'author' => __( 'woocommerce', 'woocommerce' ) === $note->comment_author ? 'system' : $note->comment_author,
'date_created' => wc_rest_prepare_date_response( $note->comment_date ),
'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ),
'note' => $note->comment_content,
'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $note ) );
/**
* Filter order note object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $note Order note object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request );
}
/**
* Create a single order note.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order || $this->post_type !== $order->get_type() ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
// Create the note.
$note_id = $order->add_order_note( $request['note'], $request['customer_note'], $request['added_by_user'] );
if ( ! $note_id ) {
return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) );
}
$note = get_comment( $note_id );
$this->update_additional_fields_for_object( $note, $request );
/**
* Fires after a order note is created or updated via the REST API.
*
* @param WP_Comment $note New order note object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( 'woocommerce_rest_insert_order_note', $note, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $note, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P<order_id>[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) );
return $response;
}
/**
* Get the Order Notes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'order_note',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'author' => array(
'description' => __( 'Order note author.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'note' => array(
'description' => __( 'Order note content.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'customer_note' => array(
'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'added_by_user' => array(
'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-order-refunds-controller.php 0000644 00000007335 15154275246 0020743 0 ustar 00 <?php
/**
* REST API Order Refunds controller
*
* Handles requests to the /orders/<order_id>/refunds endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Internal\RestApiUtil;
/**
* REST API Order Refunds controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Order_Refunds_V2_Controller
*/
class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Prepares one object for create or update operation.
*
* @since 3.0.0
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data The prepared item, or WP_Error object on failure.
*/
protected function prepare_object_for_database( $request, $creating = false ) {
RestApiUtil::adjust_create_refund_request_parameters( $request );
$order = wc_get_order( (int) $request['order_id'] );
if ( ! $order ) {
return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 );
}
if ( 0 > $request['amount'] ) {
return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 );
}
// Create the refund.
$refund = wc_create_refund(
array(
'order_id' => $order->get_id(),
'amount' => $request['amount'],
'reason' => $request['reason'],
'line_items' => $request['line_items'],
'refund_payment' => $request['api_refund'],
'restock_items' => $request['api_restock'],
)
);
if ( is_wp_error( $refund ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 );
}
if ( ! $refund ) {
return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 );
}
if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
$refund->save_meta_data();
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $coupon Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating );
}
/**
* Get the refund schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['line_items']['items']['properties']['refund_total'] = array(
'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ),
'type' => 'number',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array(
'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['api_restock'] = array(
'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'edit' ),
'default' => true,
);
return $schema;
}
}
Controllers/Version3/class-wc-rest-orders-controller.php 0000644 00000023522 15154275246 0017456 0 ustar 00 <?php
/**
* REST API Orders controller
*
* Handles requests to the /orders endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API Orders controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Orders_V2_Controller
*/
class WC_REST_Orders_Controller extends WC_REST_Orders_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Calculate coupons.
*
* @throws WC_REST_Exception When fails to set any item.
* @param WP_REST_Request $request Request object.
* @param WC_Order $order Order data.
* @return bool
*/
protected function calculate_coupons( $request, $order ) {
if ( ! isset( $request['coupon_lines'] ) ) {
return false;
}
// Validate input and at the same time store the processed coupon codes to apply.
$coupon_codes = array();
$discounts = new WC_Discounts( $order );
$current_order_coupons = array_values( $order->get_coupons() );
$current_order_coupon_codes = array_map(
function( $coupon ) {
return $coupon->get_code();
},
$current_order_coupons
);
foreach ( $request['coupon_lines'] as $item ) {
if ( ! empty( $item['id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 );
}
$coupon_code = ArrayUtil::get_value_or_default( $item, 'code' );
if ( StringUtil::is_null_or_whitespace( $coupon_code ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 );
}
$coupon_code = wc_format_coupon_code( wc_clean( $coupon_code ) );
$coupon = new WC_Coupon( $coupon_code );
// Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons.
if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) {
$check_result = $discounts->is_coupon_valid( $coupon );
if ( is_wp_error( $check_result ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 );
}
}
$coupon_codes[] = $coupon_code;
}
// Remove all coupons first to ensure calculation is correct.
foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) {
$order->remove_coupon( $existing_coupon->get_code() );
}
// Apply the coupons.
foreach ( $coupon_codes as $new_coupon ) {
$results = $order->apply_coupon( $new_coupon );
if ( is_wp_error( $results ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 );
}
}
return true;
}
/**
* Prepare a single order for create or update.
*
* @throws WC_REST_Exception When fails to set any item.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
$order = new WC_Order( $id );
$schema = $this->get_item_schema();
$data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) );
// Handle all writable props.
foreach ( $data_keys as $key ) {
$value = $request[ $key ];
if ( ! is_null( $value ) ) {
switch ( $key ) {
case 'coupon_lines':
case 'status':
// Change should be done later so transitions have new data.
break;
case 'billing':
case 'shipping':
$this->update_address( $order, $value, $key );
break;
case 'line_items':
case 'shipping_lines':
case 'fee_lines':
if ( is_array( $value ) ) {
foreach ( $value as $item ) {
if ( is_array( $item ) ) {
if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) {
$order->remove_item( $item['id'] );
} else {
$this->set_item( $order, $key, $item );
}
}
}
}
break;
case 'meta_data':
if ( is_array( $value ) ) {
foreach ( $value as $meta ) {
$order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
break;
default:
if ( is_callable( array( $order, "set_{$key}" ) ) ) {
$order->{"set_{$key}"}( $value );
}
break;
}
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @since 7.4.0
*
* @param WC_Data $order Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating );
}
/**
* Save an object data.
*
* @since 3.0.0
* @throws WC_REST_Exception But all errors are validated before returning any data.
* @param WP_REST_Request $request Full details about the request.
* @param bool $creating If is creating a new object.
* @return WC_Data|WP_Error
*/
protected function save_object( $request, $creating = false ) {
try {
$object = $this->prepare_object_for_database( $request, $creating );
if ( is_wp_error( $object ) ) {
return $object;
}
// Make sure gateways are loaded so hooks from gateways fire on save/create.
WC()->payment_gateways();
if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) {
// Make sure customer exists.
if ( false === get_user_by( 'id', $request['customer_id'] ) ) {
throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 );
}
// Make sure customer is part of blog.
if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) {
add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' );
}
}
if ( $creating ) {
$object->set_created_via( 'rest-api' );
$object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
$object->save();
$object->calculate_totals();
} else {
// If items have changed, recalculate order totals.
if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) {
$object->calculate_totals( true );
}
}
// Set coupons.
$this->calculate_coupons( $request, $object );
// Set status.
if ( ! empty( $request['status'] ) ) {
$object->set_status( $request['status'] );
}
$object->save();
// Actions for after the order is saved.
if ( true === $request['set_paid'] ) {
if ( $creating || $object->needs_payment() ) {
$object->payment_complete( $request['transaction_id'] );
}
}
return $this->get_object( $object->get_id() );
} catch ( WC_Data_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() );
} catch ( WC_REST_Exception $e ) {
return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) );
}
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
// This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query.
$statuses = $request['status'];
unset( $request['status'] );
// Prevents WC_REST_Orders_V2_Controller::prepare_objects_query() from generating a meta_query for 'customer'.
// which COT can handle as a native field.
$cot_customer =
( OrderUtil::custom_orders_table_usage_is_enabled() && isset( $request['customer'] ) )
? $request['customer']
: null;
if ( ! is_null( $cot_customer ) ) {
unset( $request['customer'] );
}
$args = parent::prepare_objects_query( $request );
$args['post_status'] = array();
foreach ( $statuses as $status ) {
if ( in_array( $status, $this->get_order_statuses(), true ) ) {
$args['post_status'][] = 'wc-' . $status;
} elseif ( 'any' === $status ) {
// Set status to "any" and short-circuit out.
$args['post_status'] = 'any';
break;
} else {
$args['post_status'][] = $status;
}
}
// Put the statuses back for further processing (next/prev links, etc).
$request['status'] = $statuses;
// Add back 'customer' to args and request.
if ( ! is_null( $cot_customer ) ) {
$args['customer'] = $cot_customer;
$request['customer'] = $cot_customer;
}
return $args;
}
/**
* Get the Order's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true;
return $schema;
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['status'] = array(
'default' => 'any',
'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ),
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}
Controllers/Version3/class-wc-rest-payment-gateways-controller.php 0000644 00000017437 15154275246 0021467 0 ustar 00 <?php
/**
* REST API WC Payment gateways controller
*
* Handles requests to the /payment_gateways endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Paymenga gateways controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Payment_Gateways_V2_Controller
*/
class WC_REST_Payment_Gateways_Controller extends WC_REST_Payment_Gateways_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Prepare a payment gateway for response.
*
* @param WC_Payment_Gateway $gateway Payment gateway object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $gateway, $request ) {
$order = (array) get_option( 'woocommerce_gateway_order' );
$item = array(
'id' => $gateway->id,
'title' => $gateway->title,
'description' => $gateway->description,
'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '',
'enabled' => ( 'yes' === $gateway->enabled ),
'method_title' => $gateway->get_method_title(),
'method_description' => $gateway->get_method_description(),
'method_supports' => $gateway->supports,
'settings' => $this->get_settings( $gateway ),
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $item, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $gateway, $request ) );
/**
* Filter payment gateway objects returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WC_Payment_Gateway $gateway Payment gateway object.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request );
}
/**
* Return settings associated with this payment gateway.
*
* @param WC_Payment_Gateway $gateway Gateway instance.
*
* @return array
*/
public function get_settings( $gateway ) {
$settings = array();
$gateway->init_form_fields();
foreach ( $gateway->form_fields as $id => $field ) {
// Make sure we at least have a title and type.
if ( empty( $field['title'] ) || empty( $field['type'] ) ) {
continue;
}
// Ignore 'enabled' and 'description' which get included elsewhere.
if ( in_array( $id, array( 'enabled', 'description' ), true ) ) {
continue;
}
$data = array(
'id' => $id,
'label' => empty( $field['label'] ) ? $field['title'] : $field['label'],
'description' => empty( $field['description'] ) ? '' : $field['description'],
'type' => $field['type'],
'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ],
'default' => empty( $field['default'] ) ? '' : $field['default'],
'tip' => empty( $field['description'] ) ? '' : $field['description'],
'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'],
);
if ( ! empty( $field['options'] ) ) {
$data['options'] = $field['options'];
}
$settings[ $id ] = $data;
}
return $settings;
}
/**
* Get the payment gateway schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'payment_gateway',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Payment gateway ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'title' => array(
'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'order' => array(
'description' => __( 'Payment gateway sort order.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'absint',
),
),
'enabled' => array(
'description' => __( 'Payment gateway enabled status.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
),
'method_title' => array(
'description' => __( 'Payment gateway method title.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_description' => array(
'description' => __( 'Payment gateway method description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'method_supports' => array(
'description' => __( 'Supported features for this payment gateway.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'string',
),
),
'settings' => array(
'description' => __( 'Payment gateway settings.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Type of setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tip' => array(
'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'placeholder' => array(
'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-posts-controller.php 0000644 00000056526 15154275246 0017342 0 ustar 00 <?php
/**
* Abstract Rest Posts Controller Class
*
* @class WC_REST_Posts_Controller
* @package WooCommerce\RestApi
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_REST_Posts_Controller
*
* @package WooCommerce\RestApi
* @version 2.6.0
*/
abstract class WC_REST_Posts_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = '';
/**
* Post type.
*
* @var string
*/
protected $post_type = '';
/**
* Controls visibility on frontend.
*
* @var string
*/
protected $public = false;
/**
* Check if a given request has access to read items.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$post = get_post( (int) $request['id'] );
if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$post = get_post( (int) $request['id'] );
if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete an item.
*
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function delete_item_permissions_check( $request ) {
$post = get_post( (int) $request['id'] );
if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
*
* @return boolean|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$post = get_post( $id );
if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$data = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $data );
if ( $this->public ) {
$response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) );
}
return $response;
}
/**
* Create a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) );
}
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
$post->post_type = $this->post_type;
$post_id = wp_insert_post( $post, true );
if ( is_wp_error( $post_id ) ) {
if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post->ID = $post_id;
$post = get_post( $post_id );
$this->update_additional_fields_for_object( $post, $request );
// Add meta fields.
$meta_fields = $this->add_post_meta_fields( $post, $request );
if ( is_wp_error( $meta_fields ) ) {
// Remove post.
$this->delete_post( $post );
return $meta_fields;
}
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) );
return $response;
}
/**
* Add post meta fields.
*
* @param WP_Post $post Post Object.
* @param WP_REST_Request $request WP_REST_Request Object.
* @return bool|WP_Error
*/
protected function add_post_meta_fields( $post, $request ) {
return true;
}
/**
* Delete post.
*
* @param WP_Post $post Post object.
*/
protected function delete_post( $post ) {
wp_delete_post( $post->ID, true );
}
/**
* Update a single post.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$id = (int) $request['id'];
$post = get_post( $id );
if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) {
return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) );
} elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) );
}
$post = $this->prepare_item_for_database( $request );
if ( is_wp_error( $post ) ) {
return $post;
}
// Convert the post object to an array, otherwise wp_update_post will expect non-escaped input.
$post_id = wp_update_post( (array) $post, true );
if ( is_wp_error( $post_id ) ) {
if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) {
$post_id->add_data( array( 'status' => 500 ) );
} else {
$post_id->add_data( array( 'status' => 400 ) );
}
return $post_id;
}
$post = get_post( $post_id );
$this->update_additional_fields_for_object( $post, $request );
// Update meta fields.
$meta_fields = $this->update_post_meta_fields( $post, $request );
if ( is_wp_error( $meta_fields ) ) {
return $meta_fields;
}
/**
* Fires after a single item is created or updated via the REST API.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating item, false when updating.
*/
do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
}
/**
* Get a collection of posts.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_items( $request ) {
$args = array();
$args['offset'] = $request['offset'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
$args['paged'] = $request['page'];
$args['post__in'] = $request['include'];
$args['post__not_in'] = $request['exclude'];
$args['posts_per_page'] = $request['per_page'];
$args['name'] = $request['slug'];
$args['post_parent__in'] = $request['parent'];
$args['post_parent__not_in'] = $request['parent_exclude'];
$args['s'] = $request['search'];
$args['date_query'] = array();
// Set before into date query. Date query must be specified as an array of an array.
if ( isset( $request['before'] ) ) {
$args['date_query'][0]['before'] = $request['before'];
}
// Set after into date query. Date query must be specified as an array of an array.
if ( isset( $request['after'] ) ) {
$args['date_query'][0]['after'] = $request['after'];
}
if ( 'wc/v1' === $this->namespace ) {
if ( is_array( $request['filter'] ) ) {
$args = array_merge( $args, $request['filter'] );
unset( $args['filter'] );
}
}
// Force the post_type argument, since it's not a user input variable.
$args['post_type'] = $this->post_type;
/**
* Filter the query arguments for a request.
*
* Enables adding extra arguments or setting defaults for a post
* collection request.
*
* @param array $args Key value array of query var to query value.
* @param WP_REST_Request $request The request used.
*/
$args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request );
$query_args = $this->prepare_items_query( $args, $request );
$posts_query = new WP_Query();
$query_result = $posts_query->query( $query_args );
$posts = array();
foreach ( $query_result as $post ) {
if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) {
continue;
}
$data = $this->prepare_item_for_response( $post, $request );
$posts[] = $this->prepare_response_for_collection( $data );
}
$page = (int) $query_args['paged'];
$total_posts = $posts_query->found_posts;
if ( $total_posts < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $query_args['paged'] );
$count_query = new WP_Query();
$count_query->query( $query_args );
$total_posts = $count_query->found_posts;
}
$max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] );
$response = rest_ensure_response( $posts );
$response->header( 'X-WP-Total', (int) $total_posts );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$request_params = $request->get_query_params();
if ( ! empty( $request_params['filter'] ) ) {
// Normalize the pagination params.
unset( $request_params['filter']['posts_per_page'] );
unset( $request_params['filter']['paged'] );
}
$base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Delete a single item.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$id = (int) $request['id'];
$force = (bool) $request['force'];
$post = get_post( $id );
if ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) {
return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) );
}
$supports_trash = EMPTY_TRASH_DAYS > 0;
/**
* Filter whether an item is trashable.
*
* Return false to disable trash support for the item.
*
* @param boolean $supports_trash Whether the item type support trashing.
* @param WP_Post $post The Post object being considered for trashing support.
*/
$supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post );
if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) {
/* translators: %s: post type */
return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $post, $request );
// If we're forcing, then delete permanently.
if ( $force ) {
$result = wp_delete_post( $id, true );
} else {
// If we don't support trashing for this type, error out.
if ( ! $supports_trash ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) );
}
// Otherwise, only trash if we haven't already.
if ( 'trash' === $post->post_status ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) );
}
// (Note that internally this falls through to `wp_delete_post` if
// the trash is disabled.)
$result = wp_trash_post( $id );
}
if ( ! $result ) {
/* translators: %s: post type */
return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) );
}
/**
* Fires after a single item is deleted or trashed via the REST API.
*
* @param object $post The deleted or trashed item.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request );
return $response;
}
/**
* Prepare links for the request.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return array Links for the given post.
*/
protected function prepare_links( $post, $request ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
return $links;
}
/**
* Determine the allowed query_vars for a get_items() response and
* prepare for WP_Query.
*
* @param array $prepared_args Prepared arguments.
* @param WP_REST_Request $request Request object.
* @return array $query_args
*/
protected function prepare_items_query( $prepared_args = array(), $request = null ) {
$valid_vars = array_flip( $this->get_allowed_query_vars() );
$query_args = array();
foreach ( $valid_vars as $var => $index ) {
if ( isset( $prepared_args[ $var ] ) ) {
/**
* Filter the query_vars used in `get_items` for the constructed query.
*
* The dynamic portion of the hook name, $var, refers to the query_var key.
*
* @param mixed $prepared_args[ $var ] The query_var value.
*/
$query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] );
}
}
$query_args['ignore_sticky_posts'] = true;
if ( 'include' === $query_args['orderby'] ) {
$query_args['orderby'] = 'post__in';
} elseif ( 'id' === $query_args['orderby'] ) {
$query_args['orderby'] = 'ID'; // ID must be capitalized.
} elseif ( 'slug' === $query_args['orderby'] ) {
$query_args['orderby'] = 'name';
}
return $query_args;
}
/**
* Get all the WP Query vars that are allowed for the API request.
*
* @return array
*/
protected function get_allowed_query_vars() {
global $wp;
/**
* Filter the publicly allowed query vars.
*
* Allows adjusting of the default query vars that are made public.
*
* @param array Array of allowed WP_Query query vars.
*/
$valid_vars = apply_filters( 'query_vars', $wp->public_query_vars );
$post_type_obj = get_post_type_object( $this->post_type );
if ( current_user_can( $post_type_obj->cap->edit_posts ) ) {
/**
* Filter the allowed 'private' query vars for authorized users.
*
* If the user has the `edit_posts` capability, we also allow use of
* private query parameters, which are only undesirable on the
* frontend, but are safe for use in query strings.
*
* To disable anyway, use
* `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );`
*
* @param array $private_query_vars Array of allowed query vars for authorized users.
* }
*/
$private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars );
$valid_vars = array_merge( $valid_vars, $private );
}
// Define our own in addition to WP's normal vars.
$rest_valid = array(
'date_query',
'ignore_sticky_posts',
'offset',
'post__in',
'post__not_in',
'post_parent',
'post_parent__in',
'post_parent__not_in',
'posts_per_page',
'meta_query',
'tax_query',
'meta_key',
'meta_value',
'meta_compare',
'meta_value_num',
);
$valid_vars = array_merge( $valid_vars, $rest_valid );
/**
* Filter allowed query vars for the REST API.
*
* This filter allows you to add or remove query vars from the final allowed
* list for all requests, including unauthenticated ones. To alter the
* vars for editors only.
*
* @param array {
* Array of allowed WP_Query query vars.
*
* @param string $allowed_query_var The query var to allow.
* }
*/
$valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars );
return $valid_vars;
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['context']['default'] = 'view';
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'id',
'include',
'title',
'slug',
'modified',
),
'validate_callback' => 'rest_validate_request_arg',
);
$post_type_obj = get_post_type_object( $this->post_type );
if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) {
$params['parent'] = array(
'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
$params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'sanitize_callback' => 'wp_parse_id_list',
'default' => array(),
);
}
if ( 'wc/v1' === $this->namespace ) {
$params['filter'] = array(
'type' => 'object',
'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ),
);
}
return $params;
}
/**
* Update post meta fields.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return bool|WP_Error
*/
protected function update_post_meta_fields( $post, $request ) {
return true;
}
}
Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php 0000644 00000001101 15154275246 0022756 0 ustar 00 <?php
/**
* REST API Product Attribute Terms controller
*
* Handles requests to the products/attributes/<attribute_id>/terms endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Attribute Terms controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Attribute_Terms_V2_Controller
*/
class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-product-attributes-controller.php 0000644 00000006467 15154275246 0022035 0 ustar 00 <?php
/**
* REST API Product Attributes controller
*
* Handles requests to the products/attributes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Attributes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Attributes_V2_Controller
*/
class WC_REST_Product_Attributes_Controller extends WC_REST_Product_Attributes_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Generates a unique slug for a given attribute name. We do this so that we can
* create more than one attribute with the same name.
*
* @param string $attribute_name The attribute name to generate a slug for.
* @return string The auto-generated slug
*/
private function generate_unique_slug( $attribute_name ) {
global $wpdb;
$root_slug = wc_sanitize_taxonomy_name( $attribute_name );
$results = $wpdb->get_results(
$wpdb->prepare( "SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name LIKE %s ORDER BY attribute_id DESC LIMIT 1", $root_slug . '%' )
);
// The slug is already unique!
if ( empty( $results ) ) {
return $root_slug;
}
$last_created_slug = $results[0]->attribute_name;
$suffix = intval( substr( $last_created_slug, strrpos( $last_created_slug, '-' ) + 1 ) );
return $root_slug . '-' . ( $suffix + 1 );
}
/**
* Create a single attribute.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
global $wpdb;
$generate_slug = stripslashes( $request['generate_slug'] );
$slug = wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) );
if ( ! empty( $generate_slug ) && 'true' === $generate_slug ) {
$slug = $this->generate_unique_slug( $request['name'] );
}
$id = wc_create_attribute(
array(
'name' => $request['name'],
'slug' => $slug,
'type' => ! empty( $request['type'] ) ? $request['type'] : 'select',
'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order',
'has_archives' => true === $request['has_archives'],
)
);
// Checks for errors.
if ( is_wp_error( $id ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) );
}
$attribute = $this->get_attribute( $id );
if ( is_wp_error( $attribute ) ) {
return $attribute;
}
$this->update_additional_fields_for_object( $attribute, $request );
/**
* Fires after a single product attribute is created or updated via the REST API.
*
* @param stdObject $attribute Inserted attribute object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating attribute, false when updating.
*/
do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $attribute, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) );
return $response;
}
}
Controllers/Version3/class-wc-rest-product-categories-controller.php 0000644 00000021612 15154275246 0021761 0 ustar 00 <?php
/**
* REST API Product Categories controller
*
* Handles requests to the products/categories endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Categories controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Categories_V2_Controller
*/
class WC_REST_Product_Categories_Controller extends WC_REST_Product_Categories_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Prepare a single product category output for response.
*
* @param WP_Term $item Term object.
* @param WP_REST_Request $request Request instance.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $item, $request ) {
// Get category display type.
$display_type = get_term_meta( $item->term_id, 'display_type', true );
// Get category order.
$menu_order = get_term_meta( $item->term_id, 'order', true );
$data = array(
'id' => (int) $item->term_id,
'name' => $item->name,
'slug' => $item->slug,
'parent' => (int) $item->parent,
'description' => $item->description,
'display' => $display_type ? $display_type : 'default',
'image' => null,
'menu_order' => (int) $menu_order,
'count' => (int) $item->count,
);
// Get category image.
$image_id = get_term_meta( $item->term_id, 'thumbnail_id', true );
if ( $image_id ) {
$attachment = get_post( $image_id );
$data['image'] = array(
'id' => (int) $image_id,
'date_created' => wc_rest_prepare_date_response( $attachment->post_date ),
'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ),
'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ),
'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ),
'src' => wp_get_attachment_url( $image_id ),
'name' => get_the_title( $attachment ),
'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ),
);
}
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $item, $request ) );
/**
* Filter a term item returned from the API.
*
* Allows modification of the term data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $item The original term object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request );
}
/**
* Get the Category schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->taxonomy,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
),
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
),
'parent' => array(
'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'HTML description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'display' => array(
'description' => __( 'Category archive display type.', 'woocommerce' ),
'type' => 'string',
'default' => 'default',
'enum' => array( 'default', 'products', 'subcategories', 'both' ),
'context' => array( 'view', 'edit' ),
),
'image' => array(
'description' => __( 'Image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'count' => array(
'description' => __( 'Number of published products for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Update term meta fields.
*
* @param WP_Term $term Term object.
* @param WP_REST_Request $request Request instance.
* @return bool|WP_Error
*
* @since 3.5.5
*/
protected function update_term_meta_fields( $term, $request ) {
$id = (int) $term->term_id;
if ( isset( $request['display'] ) ) {
update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] );
}
if ( isset( $request['menu_order'] ) ) {
update_term_meta( $id, 'order', $request['menu_order'] );
}
if ( isset( $request['image'] ) ) {
if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) );
if ( is_wp_error( $upload ) ) {
return $upload;
}
$image_id = wc_rest_set_uploaded_image_as_attachment( $upload );
} else {
$image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0;
}
// Check if image_id is a valid image attachment before updating the term meta.
if ( $image_id && wp_attachment_is_image( $image_id ) ) {
update_term_meta( $id, 'thumbnail_id', $image_id );
// Set the image alt.
if ( ! empty( $request['image']['alt'] ) ) {
update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) );
}
// Set the image title.
if ( ! empty( $request['image']['name'] ) ) {
wp_update_post(
array(
'ID' => $image_id,
'post_title' => wc_clean( $request['image']['name'] ),
)
);
}
} else {
delete_term_meta( $id, 'thumbnail_id' );
}
}
return true;
}
}
Controllers/Version3/class-wc-rest-product-reviews-controller.php 0000644 00000115534 15154275246 0021327 0 ustar 00 <?php
/**
* REST API Product Reviews Controller
*
* Handles requests to /products/reviews.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Reviews Controller Class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
class WC_REST_Product_Reviews_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'products/reviews';
/**
* Register the routes for product reviews.
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array(
'product_id' => array(
'required' => true,
'description' => __( 'Unique identifier for the product.', 'woocommerce' ),
'type' => 'integer',
),
'review' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Review content.', 'woocommerce' ),
),
'reviewer' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Name of the reviewer.', 'woocommerce' ),
),
'reviewer_email' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Email of the reviewer.', 'woocommerce' ),
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check whether a given request has permission to read webhook deliveries.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'read', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create a new product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'edit', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete a product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'delete', (int) $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
* @return boolean|WP_Error
*/
public function batch_items_permissions_check( $request ) {
if ( ! wc_rest_check_product_reviews_permissions( 'batch' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all reviews.
*
* @param WP_REST_Request $request Full details about the request.
* @return array|WP_Error
*/
public function get_items( $request ) {
// Retrieve the list of registered collection query parameters.
$registered = $this->get_collection_params();
/*
* This array defines mappings between public API query parameters whose
* values are accepted as-passed, and their internal WP_Query parameter
* name equivalents (some are the same). Only values which are also
* present in $registered will be set.
*/
$parameter_mappings = array(
'reviewer' => 'author__in',
'reviewer_email' => 'author_email',
'reviewer_exclude' => 'author__not_in',
'exclude' => 'comment__not_in',
'include' => 'comment__in',
'offset' => 'offset',
'order' => 'order',
'per_page' => 'number',
'product' => 'post__in',
'search' => 'search',
'status' => 'status',
);
$prepared_args = array();
/*
* For each known parameter which is both registered and present in the request,
* set the parameter's value on the query $prepared_args.
*/
foreach ( $parameter_mappings as $api_param => $wp_param ) {
if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
$prepared_args[ $wp_param ] = $request[ $api_param ];
}
}
// Ensure certain parameter values default to empty strings.
foreach ( array( 'author_email', 'search' ) as $param ) {
if ( ! isset( $prepared_args[ $param ] ) ) {
$prepared_args[ $param ] = '';
}
}
if ( isset( $registered['orderby'] ) ) {
$prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
}
if ( isset( $prepared_args['status'] ) ) {
$prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status'];
}
$prepared_args['no_found_rows'] = false;
$prepared_args['date_query'] = array();
// Set before into date query. Date query must be specified as an array of an array.
if ( isset( $registered['before'], $request['before'] ) ) {
$prepared_args['date_query'][0]['before'] = $request['before'];
}
// Set after into date query. Date query must be specified as an array of an array.
if ( isset( $registered['after'], $request['after'] ) ) {
$prepared_args['date_query'][0]['after'] = $request['after'];
}
if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
$prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
}
/**
* Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API.
*
* @since 3.5.0
* @link https://developer.wordpress.org/reference/classes/wp_comment_query/
* @param array $prepared_args Array of arguments for WP_Comment_Query.
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request );
// Make sure that returns only reviews.
$prepared_args['type'] = 'review';
// Query reviews.
$query = new WP_Comment_Query();
$query_result = $query->query( $prepared_args );
$reviews = array();
foreach ( $query_result as $review ) {
if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) {
continue;
}
$data = $this->prepare_item_for_response( $review, $request );
$reviews[] = $this->prepare_response_for_collection( $data );
}
$total_reviews = (int) $query->found_comments;
$max_pages = (int) $query->max_num_pages;
if ( $total_reviews < 1 ) {
// Out-of-bounds, run the query again without LIMIT for total count.
unset( $prepared_args['number'], $prepared_args['offset'] );
$query = new WP_Comment_Query();
$prepared_args['count'] = true;
$total_reviews = $query->query( $prepared_args );
$max_pages = ceil( $total_reviews / $request['per_page'] );
}
$response = rest_ensure_response( $reviews );
$response->header( 'X-WP-Total', $total_reviews );
$response->header( 'X-WP-TotalPages', $max_pages );
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $request['page'] > 1 ) {
$prev_page = $request['page'] - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $request['page'] ) {
$next_page = $request['page'] + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Create a single review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function create_item( $request ) {
if ( ! empty( $request['id'] ) ) {
return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) );
}
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
$prepared_review = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_review ) ) {
return $prepared_review;
}
$prepared_review['comment_type'] = 'review';
/*
* Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission().
*/
if ( empty( $prepared_review['comment_content'] ) ) {
return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
}
// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
if ( ! isset( $prepared_review['comment_date_gmt'] ) ) {
$prepared_review['comment_date_gmt'] = current_time( 'mysql', true );
}
if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok.
$prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok.
} else {
$prepared_review['comment_author_IP'] = '127.0.0.1';
}
if ( ! empty( $request['author_user_agent'] ) ) {
$prepared_review['comment_agent'] = $request['author_user_agent'];
} elseif ( $request->get_header( 'user_agent' ) ) {
$prepared_review['comment_agent'] = $request->get_header( 'user_agent' );
} else {
$prepared_review['comment_agent'] = '';
}
$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review );
if ( is_wp_error( $check_comment_lengths ) ) {
$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
}
$prepared_review['comment_parent'] = 0;
$prepared_review['comment_author_url'] = '';
$prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true );
if ( is_wp_error( $prepared_review['comment_approved'] ) ) {
$error_code = $prepared_review['comment_approved']->get_error_code();
$error_message = $prepared_review['comment_approved']->get_error_message();
if ( 'comment_duplicate' === $error_code ) {
return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) );
}
if ( 'comment_flood' === $error_code ) {
return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) );
}
return $prepared_review['comment_approved'];
}
/**
* Filters a review before it is inserted via the REST API.
*
* Allows modification of the review right before it is inserted via wp_insert_comment().
* Returning a WP_Error value from the filter will shortcircuit insertion and allow
* skipping further processing.
*
* @since 3.5.0
* @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment().
* @param WP_REST_Request $request Request used to insert the review.
*/
$prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request );
if ( is_wp_error( $prepared_review ) ) {
return $prepared_review;
}
$review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) );
if ( ! $review_id ) {
return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
if ( isset( $request['status'] ) ) {
$this->handle_status_param( $request['status'], $review_id );
}
update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' );
$review = get_comment( $review_id );
/**
* Fires after a comment is created or updated via the REST API.
*
* @param WP_Comment $review Inserted or updated comment object.
* @param WP_REST_Request $request Request object.
* @param bool $creating True when creating a comment, false when updating.
*/
do_action( 'woocommerce_rest_insert_product_review', $review, $request, true );
$fields_update = $this->update_additional_fields_for_object( $review, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
$request->set_param( 'context', $context );
$response = $this->prepare_item_for_response( $review, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) );
return $response;
}
/**
* Get a single product review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function get_item( $request ) {
$review = $this->get_review( $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
$data = $this->prepare_item_for_response( $review, $request );
$response = rest_ensure_response( $data );
return $response;
}
/**
* Updates a review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
*/
public function update_item( $request ) {
$review = $this->get_review( $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
$id = (int) $review->comment_ID;
if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) {
return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) );
}
$prepared_args = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_args ) ) {
return $prepared_args;
}
if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
}
if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
// Only the comment status is being changed.
$change = $this->handle_status_param( $request['status'], $id );
if ( ! $change ) {
return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
} elseif ( ! empty( $prepared_args ) ) {
if ( is_wp_error( $prepared_args ) ) {
return $prepared_args;
}
if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) );
}
$prepared_args['comment_ID'] = $id;
$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );
if ( is_wp_error( $check_comment_lengths ) ) {
$error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() );
return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) );
}
$updated = wp_update_comment( wp_slash( (array) $prepared_args ) );
if ( false === $updated ) {
return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
if ( isset( $request['status'] ) ) {
$this->handle_status_param( $request['status'], $id );
}
}
if ( ! empty( $request['rating'] ) ) {
update_comment_meta( $id, 'rating', $request['rating'] );
}
$review = get_comment( $id );
/** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */
do_action( 'woocommerce_rest_insert_product_review', $review, $request, false );
$fields_update = $this->update_additional_fields_for_object( $review, $request );
if ( is_wp_error( $fields_update ) ) {
return $fields_update;
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $review, $request );
return rest_ensure_response( $response );
}
/**
* Deletes a review.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or error object on failure.
*/
public function delete_item( $request ) {
$review = $this->get_review( $request['id'] );
if ( is_wp_error( $review ) ) {
return $review;
}
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
/**
* Filters whether a review can be trashed.
*
* Return false to disable trash support for the post.
*
* @since 3.5.0
* @param bool $supports_trash Whether the post type support trashing.
* @param WP_Comment $review The review object being considered for trashing support.
*/
$supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review );
$request->set_param( 'context', 'edit' );
if ( $force ) {
$previous = $this->prepare_item_for_response( $review, $request );
$result = wp_delete_comment( $review->comment_ID, true );
$response = new WP_REST_Response();
$response->set_data(
array(
'deleted' => true,
'previous' => $previous->get_data(),
)
);
} else {
// If this type doesn't support trashing, error out.
if ( ! $supports_trash ) {
/* translators: %s: force=true */
return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) );
}
if ( 'trash' === $review->comment_approved ) {
return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) );
}
$result = wp_trash_comment( $review->comment_ID );
$review = get_comment( $review->comment_ID );
$response = $this->prepare_item_for_response( $review, $request );
}
if ( ! $result ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
/**
* Fires after a review is deleted via the REST API.
*
* @param WP_Comment $review The deleted review data.
* @param WP_REST_Response $response The response returned from the API.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( 'woocommerce_rest_delete_review', $review, $response, $request );
return $response;
}
/**
* Prepare a single product review output for response.
*
* @param WP_Comment $review Product review object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $review, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$fields = $this->get_fields_for_response( $request );
$data = array();
if ( in_array( 'id', $fields, true ) ) {
$data['id'] = (int) $review->comment_ID;
}
if ( in_array( 'date_created', $fields, true ) ) {
$data['date_created'] = wc_rest_prepare_date_response( $review->comment_date );
}
if ( in_array( 'date_created_gmt', $fields, true ) ) {
$data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt );
}
if ( in_array( 'product_id', $fields, true ) ) {
$data['product_id'] = (int) $review->comment_post_ID;
}
if ( in_array( 'product_name', $fields, true ) ) {
$data['product_name'] = get_the_title( (int) $review->comment_post_ID );
}
if ( in_array( 'product_permalink', $fields, true ) ) {
$data['product_permalink'] = get_permalink( (int) $review->comment_post_ID );
}
if ( in_array( 'status', $fields, true ) ) {
$data['status'] = $this->prepare_status_response( (string) $review->comment_approved );
}
if ( in_array( 'reviewer', $fields, true ) ) {
$data['reviewer'] = $review->comment_author;
}
if ( in_array( 'reviewer_email', $fields, true ) ) {
$data['reviewer_email'] = $review->comment_author_email;
}
if ( in_array( 'review', $fields, true ) ) {
$data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content;
}
if ( in_array( 'rating', $fields, true ) ) {
$data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true );
}
if ( in_array( 'verified', $fields, true ) ) {
$data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID );
}
if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) {
$data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email );
}
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $review ) );
/**
* Filter product reviews object returned from the REST API.
*
* @param WP_REST_Response $response The response object.
* @param WP_Comment $review Product review object used to create response.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request );
}
/**
* Prepare a single product review to be inserted into the database.
*
* @param WP_REST_Request $request Request object.
* @return array|WP_Error $prepared_review
*/
protected function prepare_item_for_database( $request ) {
if ( isset( $request['id'] ) ) {
$prepared_review['comment_ID'] = (int) $request['id'];
}
if ( isset( $request['review'] ) ) {
$prepared_review['comment_content'] = $request['review'];
}
if ( isset( $request['product_id'] ) ) {
$prepared_review['comment_post_ID'] = (int) $request['product_id'];
}
if ( isset( $request['reviewer'] ) ) {
$prepared_review['comment_author'] = $request['reviewer'];
}
if ( isset( $request['reviewer_email'] ) ) {
$prepared_review['comment_author_email'] = $request['reviewer_email'];
}
if ( ! empty( $request['date_created'] ) ) {
$date_data = rest_get_date_with_gmt( $request['date_created'] );
if ( ! empty( $date_data ) ) {
list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
}
} elseif ( ! empty( $request['date_created_gmt'] ) ) {
$date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true );
if ( ! empty( $date_data ) ) {
list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data;
}
}
/**
* Filters a review after it is prepared for the database.
*
* Allows modification of the review right after it is prepared for the database.
*
* @since 3.5.0
* @param array $prepared_review The prepared review data for `wp_insert_comment`.
* @param WP_REST_Request $request The current request.
*/
return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request );
}
/**
* Prepare links for the request.
*
* @param WP_Comment $review Product review object.
* @return array Links for the given product review.
*/
protected function prepare_links( $review ) {
$links = array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ),
),
);
if ( 0 !== (int) $review->comment_post_ID ) {
$links['up'] = array(
'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ),
);
}
if ( 0 !== (int) $review->user_id ) {
$links['reviewer'] = array(
'href' => rest_url( 'wp/v2/users/' . $review->user_id ),
'embeddable' => true,
);
}
return $links;
}
/**
* Get the Product Review's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'product_review',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'product_name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'product_permalink' => array(
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Status of the review.', 'woocommerce' ),
'type' => 'string',
'default' => 'approved',
'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ),
'context' => array( 'view', 'edit' ),
),
'reviewer' => array(
'description' => __( 'Reviewer name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'reviewer_email' => array(
'description' => __( 'Reviewer email.', 'woocommerce' ),
'type' => 'string',
'format' => 'email',
'context' => array( 'view', 'edit' ),
),
'review' => array(
'description' => __( 'The content of the review.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'arg_options' => array(
'sanitize_callback' => 'wp_filter_post_kses',
),
),
'rating' => array(
'description' => __( 'Review rating (0 to 5).', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'verified' => array(
'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
if ( get_option( 'show_avatars' ) ) {
$avatar_properties = array();
$avatar_sizes = rest_get_avatar_sizes();
foreach ( $avatar_sizes as $size ) {
$avatar_properties[ $size ] = array(
/* translators: %d: avatar image size in pixels */
'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'embed', 'view', 'edit' ),
);
}
$schema['properties']['reviewer_avatar_urls'] = array(
'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $avatar_properties,
);
}
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['context']['default'] = 'view';
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
);
$params['before'] = array(
'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
);
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$params['include'] = array(
'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ),
'type' => 'integer',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array(
'asc',
'desc',
),
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date_gmt',
'enum' => array(
'date',
'date_gmt',
'id',
'include',
'product',
),
);
$params['reviewer'] = array(
'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$params['reviewer_exclude'] = array(
'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$params['reviewer_email'] = array(
'default' => null,
'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ),
'format' => 'email',
'type' => 'string',
);
$params['product'] = array(
'default' => array(),
'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
);
$params['status'] = array(
'default' => 'approved',
'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ),
'sanitize_callback' => 'sanitize_key',
'type' => 'string',
'enum' => array(
'all',
'hold',
'approved',
'spam',
'trash',
),
);
/**
* Filter collection parameters for the reviews controller.
*
* This filter registers the collection parameter, but does not map the
* collection parameter to an internal WP_Comment_Query parameter. Use the
* `wc_rest_review_query` filter to set WP_Comment_Query parameters.
*
* @since 3.5.0
* @param array $params JSON Schema-formatted collection parameters.
*/
return apply_filters( 'woocommerce_rest_product_review_collection_params', $params );
}
/**
* Get the reivew, if the ID is valid.
*
* @since 3.5.0
* @param int $id Supplied ID.
* @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
*/
protected function get_review( $id ) {
$id = (int) $id;
$error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) );
if ( 0 >= $id ) {
return $error;
}
$review = get_comment( $id );
if ( empty( $review ) ) {
return $error;
}
if ( ! empty( $review->comment_post_ID ) ) {
$post = get_post( (int) $review->comment_post_ID );
if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
}
return $review;
}
/**
* Prepends internal property prefix to query parameters to match our response fields.
*
* @since 3.5.0
* @param string $query_param Query parameter.
* @return string
*/
protected function normalize_query_param( $query_param ) {
$prefix = 'comment_';
switch ( $query_param ) {
case 'id':
$normalized = $prefix . 'ID';
break;
case 'product':
$normalized = $prefix . 'post_ID';
break;
case 'include':
$normalized = 'comment__in';
break;
default:
$normalized = $prefix . $query_param;
break;
}
return $normalized;
}
/**
* Checks comment_approved to set comment status for single comment output.
*
* @since 3.5.0
* @param string|int $comment_approved comment status.
* @return string Comment status.
*/
protected function prepare_status_response( $comment_approved ) {
switch ( $comment_approved ) {
case 'hold':
case '0':
$status = 'hold';
break;
case 'approve':
case '1':
$status = 'approved';
break;
case 'spam':
case 'trash':
default:
$status = $comment_approved;
break;
}
return $status;
}
/**
* Sets the comment_status of a given review object when creating or updating a review.
*
* @since 3.5.0
* @param string|int $new_status New review status.
* @param int $id Review ID.
* @return bool Whether the status was changed.
*/
protected function handle_status_param( $new_status, $id ) {
$old_status = wp_get_comment_status( $id );
if ( $new_status === $old_status ) {
return false;
}
switch ( $new_status ) {
case 'approved':
case 'approve':
case '1':
$changed = wp_set_comment_status( $id, 'approve' );
break;
case 'hold':
case '0':
$changed = wp_set_comment_status( $id, 'hold' );
break;
case 'spam':
$changed = wp_spam_comment( $id );
break;
case 'unspam':
$changed = wp_unspam_comment( $id );
break;
case 'trash':
$changed = wp_trash_comment( $id );
break;
case 'untrash':
$changed = wp_untrash_comment( $id );
break;
default:
$changed = false;
break;
}
return $changed;
}
}
Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php 0000644 00000001067 15154275246 0023112 0 ustar 00 <?php
/**
* REST API Product Shipping Classes controller
*
* Handles requests to the products/shipping_classes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Shipping Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Shipping_Classes_V2_Controller
*/
class WC_REST_Product_Shipping_Classes_Controller extends WC_REST_Product_Shipping_Classes_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-product-tags-controller.php 0000644 00000000757 15154275246 0020601 0 ustar 00 <?php
/**
* REST API Product Tags controller
*
* Handles requests to the products/tags endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Product Tags controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Tags_V2_Controller
*/
class WC_REST_Product_Tags_Controller extends WC_REST_Product_Tags_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-product-variations-controller.php 0000644 00000106325 15154275247 0022021 0 ustar 00 <?php
/**
* REST API variations controller
*
* Handles requests to the /products/<product_id>/variations endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
use Automattic\Jetpack\Constants;
/**
* REST API variations controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Product_Variations_V2_Controller
*/
class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Register the routes for products.
*/
public function register_routes() {
parent::register_routes();
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/generate',
array(
'args' => array(
'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer',
),
'delete' => array(
'description' => __( 'Deletes unused variations.', 'woocommerce' ),
'type' => 'boolean',
),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'generate' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
* Prepare a single variation output for response.
*
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_object_for_response( $object, $request ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = array(
'id' => $object->get_id(),
'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ),
'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ),
'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ),
'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ),
'description' => wc_format_content( $object->get_description() ),
'permalink' => $object->get_permalink(),
'sku' => $object->get_sku(),
'price' => $object->get_price(),
'regular_price' => $object->get_regular_price(),
'sale_price' => $object->get_sale_price(),
'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ),
'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ),
'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ),
'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ),
'on_sale' => $object->is_on_sale(),
'status' => $object->get_status(),
'purchasable' => $object->is_purchasable(),
'virtual' => $object->is_virtual(),
'downloadable' => $object->is_downloadable(),
'downloads' => $this->get_downloads( $object ),
'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
'tax_status' => $object->get_tax_status(),
'tax_class' => $object->get_tax_class(),
'manage_stock' => $object->managing_stock(),
'stock_quantity' => $object->get_stock_quantity(),
'stock_status' => $object->get_stock_status(),
'backorders' => $object->get_backorders(),
'backorders_allowed' => $object->backorders_allowed(),
'backordered' => $object->is_on_backorder(),
'low_stock_amount' => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(),
'weight' => $object->get_weight(),
'dimensions' => array(
'length' => $object->get_length(),
'width' => $object->get_width(),
'height' => $object->get_height(),
),
'shipping_class' => $object->get_shipping_class(),
'shipping_class_id' => $object->get_shipping_class_id(),
'image' => $this->get_image( $object, $context ),
'attributes' => $this->get_attributes( $object ),
'menu_order' => $object->get_menu_order(),
'meta_data' => $object->get_meta_data(),
);
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $object, $request ) );
/**
* Filter the data for a response.
*
* The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response.
*
* @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object.
*/
return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request );
}
/**
* Prepare a single variation for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
if ( isset( $request['id'] ) ) {
$variation = wc_get_product( absint( $request['id'] ) );
} else {
$variation = new WC_Product_Variation();
}
$variation->set_parent_id( absint( $request['product_id'] ) );
// Status.
if ( isset( $request['status'] ) ) {
$variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
}
// SKU.
if ( isset( $request['sku'] ) ) {
$variation->set_sku( wc_clean( $request['sku'] ) );
}
// Thumbnail.
if ( isset( $request['image'] ) ) {
if ( is_array( $request['image'] ) ) {
$variation = $this->set_variation_image( $variation, $request['image'] );
} else {
$variation->set_image_id( '' );
}
}
// Virtual variation.
if ( isset( $request['virtual'] ) ) {
$variation->set_virtual( $request['virtual'] );
}
// Downloadable variation.
if ( isset( $request['downloadable'] ) ) {
$variation->set_downloadable( $request['downloadable'] );
}
// Downloads.
if ( $variation->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$variation = $this->save_downloadable_files( $variation, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$variation->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$variation->set_download_expiry( $request['download_expiry'] );
}
}
// Shipping data.
$variation = $this->save_product_shipping_data( $variation, $request );
// Stock handling.
if ( isset( $request['manage_stock'] ) ) {
$variation->set_manage_stock( $request['manage_stock'] );
}
if ( isset( $request['stock_status'] ) ) {
$variation->set_stock_status( $request['stock_status'] );
}
if ( isset( $request['backorders'] ) ) {
$variation->set_backorders( $request['backorders'] );
}
if ( $variation->get_manage_stock() ) {
if ( isset( $request['stock_quantity'] ) ) {
$variation->set_stock_quantity( $request['stock_quantity'] );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $variation->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$variation->set_stock_quantity( $stock_quantity );
}
// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
if ( null === $request['low_stock_amount'] ) {
$variation->set_low_stock_amount( '' );
} else {
$variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
}
}
} else {
$variation->set_backorders( 'no' );
$variation->set_stock_quantity( '' );
$variation->set_low_stock_amount( '' );
}
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$variation->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$variation->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$variation->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_from_gmt'] ) ) {
$variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$variation->set_date_on_sale_to( $request['date_on_sale_to'] );
}
if ( isset( $request['date_on_sale_to_gmt'] ) ) {
$variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
}
// Tax class.
if ( isset( $request['tax_class'] ) ) {
$variation->set_tax_class( $request['tax_class'] );
}
// Description.
if ( isset( $request['description'] ) ) {
$variation->set_description( wp_kses_post( $request['description'] ) );
}
// Update taxonomies.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
$parent = wc_get_product( $variation->get_parent_id() );
if ( ! $parent ) {
return new WP_Error(
// Translators: %d parent ID.
"woocommerce_rest_{$this->post_type}_invalid_parent",
__( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ),
array( 'status' => 404 )
);
}
$parent_attributes = $parent->get_attributes();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = sanitize_title( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) {
continue;
}
$attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() );
$attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : '';
if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) {
// If dealing with a taxonomy, we need to get the slug from the name posted to the API.
$term = get_term_by( 'name', $attribute_value, $attribute_name );
if ( $term && ! is_wp_error( $term ) ) {
$attribute_value = $term->slug;
} else {
$attribute_value = sanitize_title( $attribute_value );
}
}
$attributes[ $attribute_key ] = $attribute_value;
}
$variation->set_attributes( $attributes );
}
// Menu order.
if ( $request['menu_order'] ) {
$variation->set_menu_order( $request['menu_order'] );
}
// Meta data.
if ( is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $variation Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating );
}
/**
* Get the image for a product variation.
*
* @param WC_Product_Variation $variation Variation data.
* @param string $context Context of the request: 'view' or 'edit'.
* @return array
*/
protected function get_image( $variation, $context = 'view' ) {
if ( ! $variation->get_image_id( $context ) ) {
return;
}
$attachment_id = $variation->get_image_id();
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) ) {
return;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
return;
}
if ( ! isset( $image ) ) {
return array(
'id' => (int) $attachment_id,
'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
'src' => current( $attachment ),
'name' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
);
}
}
/**
* Set variation image.
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product_Variation $variation Variation instance.
* @param array $image Image data.
* @return WC_Product_Variation
*/
protected function set_variation_image( $variation, $image ) {
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
if ( 0 === $attachment_id ) {
if ( isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) {
throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 );
}
}
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() );
} else {
$variation->set_image_id( '' );
return $variation;
}
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {
/* translators: %s: attachment ID */
throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
}
$variation->set_image_id( $attachment_id );
// Set the image alt if present.
if ( ! empty( $image['alt'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
}
// Set the image name if present.
if ( ! empty( $image['name'] ) ) {
wp_update_post(
array(
'ID' => $attachment_id,
'post_title' => $image['name'],
)
);
}
return $variation;
}
/**
* Get the Variation's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) );
$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Variation description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Variation URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current variation price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Variation regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Variation sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from_gmt' => array(
'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to_gmt' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'on_sale' => array(
'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'status' => array(
'description' => __( 'Variation status.', 'woocommerce' ),
'type' => 'string',
'default' => 'publish',
'enum' => array_keys( get_post_statuses() ),
'context' => array( 'view', 'edit' ),
),
'purchasable' => array(
'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'virtual' => array(
'description' => __( 'If the variation is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the variation is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at variation level.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'stock_status' => array(
'description' => __( 'Controls the stock status of the product.', 'woocommerce' ),
'type' => 'string',
'default' => 'instock',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ),
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Variation dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'image' => array(
'description' => __( 'Variation image data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Prepare objects query.
*
* @since 3.0.0
* @param WP_REST_Request $request Full details about the request.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = WC_REST_CRUD_Controller::prepare_objects_query( $request );
// Set post_status.
$args['post_status'] = $request['status'];
// Filter by local attributes.
if ( ! empty( $request['local_attributes'] ) && is_array( $request['local_attributes'] ) ) {
foreach ( $request['local_attributes'] as $attribute ) {
if ( ! isset( $attribute['attribute'] ) || ! isset( $attribute['term'] ) ) {
continue;
}
$args['meta_query'] = $this->add_meta_query( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$args,
array(
'key' => 'attribute_' . $attribute['attribute'],
'value' => $attribute['term'],
)
);
}
}
// Filter by sku.
if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $request['sku'];
}
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_sku',
'value' => $skus,
'compare' => 'IN',
)
);
}
// Filter by tax class.
if ( ! empty( $request['tax_class'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_tax_class',
'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
)
);
}
// Price filter.
if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok.
}
// Price filter.
if ( is_bool( $request['has_price'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_price',
'compare' => $request['has_price'] ? 'EXISTS' : 'NOT EXISTS',
)
);
}
// Filter product based on stock_status.
if ( ! empty( $request['stock_status'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_stock_status',
'value' => $request['stock_status'],
)
);
}
// Filter by on sale products.
if ( is_bool( $request['on_sale'] ) ) {
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
$on_sale_ids = wc_get_product_ids_on_sale();
// Use 0 when there's no on sale products to avoid return all products.
$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;
$args[ $on_sale_key ] += $on_sale_ids;
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = $this->post_type;
}
$args['post_parent'] = $request['product_id'];
return $args;
}
/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
unset(
$params['in_stock'],
$params['type'],
$params['featured'],
$params['category'],
$params['tag'],
$params['shipping_class'],
$params['attribute'],
$params['attribute_term']
);
$params['stock_status'] = array(
'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['has_price'] = array(
'description' => __( 'Limit result set to products with or without price.', 'woocommerce' ),
'type' => 'boolean',
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Deletes all unmatched variations (aka duplicates).
*
* @param WC_Product $product Variable product.
* @return int Number of deleted variations.
*/
private function delete_unmatched_product_variations( $product ) {
$deleted_count = 0;
if ( ! $product ) {
return $deleted_count;
}
$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );
// Get existing variations so we don't create duplicates.
$existing_variations = array_map( 'wc_get_product', $product->get_children() );
$possible_attribute_combinations = array_reverse( wc_array_cartesian( $attributes ) );
foreach ( $existing_variations as $existing_variation ) {
$matching_attribute_key = array_search( $existing_variation->get_attributes(), $possible_attribute_combinations ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( $matching_attribute_key !== false ) {
// We only want one possible variation for each possible attribute combination.
unset( $possible_attribute_combinations[ $matching_attribute_key ] );
continue;
}
$existing_variation->delete( true );
$deleted_count ++;
}
return $deleted_count;
}
/**
* Generate all variations for a given product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response
*/
public function generate( $request ) {
$product_id = (int) $request['product_id'];
if ( 'product' !== get_post_type( $product_id ) ) {
return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) );
}
wc_maybe_define_constant( 'WC_MAX_LINKED_VARIATIONS', 50 );
wc_set_time_limit( 0 );
$response = array();
$product = wc_get_product( $product_id );
$data_store = $product->get_data_store();
$response['count'] = $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) );
if ( isset( $request['delete'] ) && $request['delete'] ) {
$deleted_count = $this->delete_unmatched_product_variations( $product );
$response['deleted_count'] = $deleted_count;
}
$data_store->sort_all_product_variations( $product->get_id() );
return rest_ensure_response( $response );
}
}
Controllers/Version3/class-wc-rest-products-controller.php 0000644 00000146166 15154275247 0020036 0 ustar 00 <?php
/**
* REST API Products controller
*
* Handles requests to the /products endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
use Automattic\WooCommerce\Utilities\I18nUtil;
defined( 'ABSPATH' ) || exit;
/**
* REST API Products controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Products_V2_Controller
*/
class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* A string to inject into a query to do a partial match SKU search.
*
* See prepare_objects_query()
*
* @var string
*/
private $search_sku_in_product_lookup_table = '';
/**
* Get the images for a product or product variation.
*
* @param WC_Product|WC_Product_Variation $product Product instance.
* @return array
*/
protected function get_images( $product ) {
$images = array();
$attachment_ids = array();
// Add featured image.
if ( $product->get_image_id() ) {
$attachment_ids[] = $product->get_image_id();
}
// Add gallery images.
$attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() );
// Build image data.
foreach ( $attachment_ids as $attachment_id ) {
$attachment_post = get_post( $attachment_id );
if ( is_null( $attachment_post ) ) {
continue;
}
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
continue;
}
$images[] = array(
'id' => (int) $attachment_id,
'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ),
'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ),
'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ),
'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ),
'src' => current( $attachment ),
'name' => get_the_title( $attachment_id ),
'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ),
);
}
return $images;
}
/**
* Make extra product orderby features supported by WooCommerce available to the WC API.
* This includes 'price', 'popularity', and 'rating'.
*
* @param WP_REST_Request $request Request data.
* @return array
*/
protected function prepare_objects_query( $request ) {
$args = WC_REST_CRUD_Controller::prepare_objects_query( $request );
// Set post_status.
$args['post_status'] = $request['status'];
// Taxonomy query to filter products by type, category,
// tag, shipping class, and attribute.
$tax_query = array();
// Map between taxonomy name and arg's key.
$taxonomies = array(
'product_cat' => 'category',
'product_tag' => 'tag',
'product_shipping_class' => 'shipping_class',
);
// Set tax_query for each passed arg.
foreach ( $taxonomies as $taxonomy => $key ) {
if ( ! empty( $request[ $key ] ) ) {
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $request[ $key ],
);
}
}
// Filter product type by slug.
if ( ! empty( $request['type'] ) ) {
$tax_query[] = array(
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $request['type'],
);
}
// Filter by attribute and term.
if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) {
if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) {
$tax_query[] = array(
'taxonomy' => $request['attribute'],
'field' => 'term_id',
'terms' => $request['attribute_term'],
);
}
}
// Build tax_query if taxonomies are set.
if ( ! empty( $tax_query ) ) {
if ( ! empty( $args['tax_query'] ) ) {
$args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok.
} else {
$args['tax_query'] = $tax_query; // WPCS: slow query ok.
}
}
// Filter featured.
if ( is_bool( $request['featured'] ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'featured',
'operator' => true === $request['featured'] ? 'IN' : 'NOT IN',
);
}
if ( wc_product_sku_enabled() ) {
// Do a partial match for a sku. Supercedes sku parameter that does exact matching.
if ( ! empty( $request['search_sku'] ) ) {
// Store this for use in the query clause filters.
$this->search_sku_in_product_lookup_table = $request['search_sku'];
unset( $request['sku'] );
}
// Filter by sku.
if ( ! empty( $request['sku'] ) ) {
$skus = explode( ',', $request['sku'] );
// Include the current string as a SKU too.
if ( 1 < count( $skus ) ) {
$skus[] = $request['sku'];
}
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_sku',
'value' => $skus,
'compare' => 'IN',
)
);
}
}
// Filter by tax class.
if ( ! empty( $request['tax_class'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_tax_class',
'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '',
)
);
}
// Price filter.
if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) {
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok.
}
// Filter product by stock_status.
if ( ! empty( $request['stock_status'] ) ) {
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
$args,
array(
'key' => '_stock_status',
'value' => $request['stock_status'],
)
);
}
// Filter by on sale products.
if ( is_bool( $request['on_sale'] ) ) {
$on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in';
$on_sale_ids = wc_get_product_ids_on_sale();
// Use 0 when there's no on sale products to avoid return all products.
$on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids;
$args[ $on_sale_key ] += $on_sale_ids;
}
// Force the post_type argument, since it's not a user input variable.
if ( ! empty( $request['sku'] ) || ! empty( $request['search_sku'] ) ) {
$args['post_type'] = array( 'product', 'product_variation' );
} else {
$args['post_type'] = $this->post_type;
}
$ordering_args = WC()->query->get_catalog_ordering_args( $args['orderby'], $args['order'] );
$args['orderby'] = $ordering_args['orderby'];
$args['order'] = $ordering_args['order'];
if ( $ordering_args['meta_key'] ) {
$args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok.
}
return $args;
}
/**
* Get objects.
*
* @param array $query_args Query args.
* @return array
*/
protected function get_objects( $query_args ) {
// Add filters for search criteria in product postmeta via the lookup table.
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
add_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
add_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );
}
$result = parent::get_objects( $query_args );
// Remove filters for search criteria in product postmeta via the lookup table.
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
remove_filter( 'posts_join', array( $this, 'add_search_criteria_to_wp_query_join' ) );
remove_filter( 'posts_where', array( $this, 'add_search_criteria_to_wp_query_where' ) );
$this->search_sku_in_product_lookup_table = '';
}
return $result;
}
/**
* Join `wc_product_meta_lookup` table when SKU search query is present.
*
* @param string $join Join clause used to search posts.
* @return string
*/
public function add_search_criteria_to_wp_query_join( $join ) {
global $wpdb;
if ( ! empty( $this->search_sku_in_product_lookup_table ) && ! strstr( $join, 'wc_product_meta_lookup' ) ) {
$join .= " LEFT JOIN $wpdb->wc_product_meta_lookup wc_product_meta_lookup
ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
}
return $join;
}
/**
* Add a where clause for matching the SKU field.
*
* @param string $where Where clause used to search posts.
* @return string
*/
public function add_search_criteria_to_wp_query_where( $where ) {
global $wpdb;
if ( ! empty( $this->search_sku_in_product_lookup_table ) ) {
$like_search = '%' . $wpdb->esc_like( $this->search_sku_in_product_lookup_table ) . '%';
$where .= ' AND ' . $wpdb->prepare( '(wc_product_meta_lookup.sku LIKE %s)', $like_search );
}
return $where;
}
/**
* Set product images.
*
* @throws WC_REST_Exception REST API exceptions.
* @param WC_Product $product Product instance.
* @param array $images Images data.
* @return WC_Product
*/
protected function set_product_images( $product, $images ) {
$images = is_array( $images ) ? array_filter( $images ) : array();
if ( ! empty( $images ) ) {
$gallery = array();
foreach ( $images as $index => $image ) {
$attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0;
if ( 0 === $attachment_id && isset( $image['src'] ) ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) {
throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
} else {
continue;
}
}
$attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() );
}
if ( ! wp_attachment_is_image( $attachment_id ) ) {
/* translators: %s: image ID */
throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 );
}
$featured_image = $product->get_image_id();
if ( 0 === $index ) {
$product->set_image_id( $attachment_id );
} else {
$gallery[] = $attachment_id;
}
// Set the image alt if present.
if ( ! empty( $image['alt'] ) ) {
update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) );
}
// Set the image name if present.
if ( ! empty( $image['name'] ) ) {
wp_update_post(
array(
'ID' => $attachment_id,
'post_title' => $image['name'],
)
);
}
}
$product->set_gallery_image_ids( $gallery );
} else {
$product->set_image_id( '' );
$product->set_gallery_image_ids( array() );
}
return $product;
}
/**
* Prepare a single product for create or update.
*
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
* @return WP_Error|WC_Data
*/
protected function prepare_object_for_database( $request, $creating = false ) {
$id = isset( $request['id'] ) ? absint( $request['id'] ) : 0;
// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $request['type'] ) ) {
$classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] );
if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}
$product = new $classname( $id );
} elseif ( isset( $request['id'] ) ) {
$product = wc_get_product( $id );
} else {
$product = new WC_Product_Simple();
}
if ( 'variation' === $product->get_type() ) {
return new WP_Error(
"woocommerce_rest_invalid_{$this->post_type}_id",
__( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ),
array(
'status' => 404,
)
);
}
// Post title.
if ( isset( $request['name'] ) ) {
$product->set_name( wp_filter_post_kses( $request['name'] ) );
}
// Post content.
if ( isset( $request['description'] ) ) {
$product->set_description( wp_filter_post_kses( $request['description'] ) );
}
// Post excerpt.
if ( isset( $request['short_description'] ) ) {
$product->set_short_description( wp_filter_post_kses( $request['short_description'] ) );
}
// Post status.
if ( isset( $request['status'] ) ) {
$product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' );
}
// Post slug.
if ( isset( $request['slug'] ) ) {
$product->set_slug( $request['slug'] );
}
// Menu order.
if ( isset( $request['menu_order'] ) ) {
$product->set_menu_order( $request['menu_order'] );
}
// Comment status.
if ( isset( $request['reviews_allowed'] ) ) {
$product->set_reviews_allowed( $request['reviews_allowed'] );
}
// Post password.
if ( isset( $request['post_password'] ) ) {
$product->set_post_password( $request['post_password'] );
}
// Virtual.
if ( isset( $request['virtual'] ) ) {
$product->set_virtual( $request['virtual'] );
}
// Tax status.
if ( isset( $request['tax_status'] ) ) {
$product->set_tax_status( $request['tax_status'] );
}
// Tax Class.
if ( isset( $request['tax_class'] ) ) {
$product->set_tax_class( $request['tax_class'] );
}
// Catalog Visibility.
if ( isset( $request['catalog_visibility'] ) ) {
$product->set_catalog_visibility( $request['catalog_visibility'] );
}
// Purchase Note.
if ( isset( $request['purchase_note'] ) ) {
$product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) );
}
// Featured Product.
if ( isset( $request['featured'] ) ) {
$product->set_featured( $request['featured'] );
}
// Shipping data.
$product = $this->save_product_shipping_data( $product, $request );
// SKU.
if ( isset( $request['sku'] ) ) {
$product->set_sku( wc_clean( $request['sku'] ) );
}
// Attributes.
if ( isset( $request['attributes'] ) ) {
$attributes = array();
foreach ( $request['attributes'] as $attribute ) {
$attribute_id = 0;
$attribute_name = '';
// Check ID for global attributes or name for product attributes.
if ( ! empty( $attribute['id'] ) ) {
$attribute_id = absint( $attribute['id'] );
$attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id );
} elseif ( ! empty( $attribute['name'] ) ) {
$attribute_name = wc_clean( $attribute['name'] );
}
if ( ! $attribute_id && ! $attribute_name ) {
continue;
}
if ( $attribute_id ) {
if ( isset( $attribute['options'] ) ) {
$options = $attribute['options'];
if ( ! is_array( $attribute['options'] ) ) {
// Text based attributes - Posted values are term names.
$options = explode( WC_DELIMITER, $options );
}
$values = array_map( 'wc_sanitize_term_text_based', $options );
$values = array_filter( $values, 'strlen' );
} else {
$values = array();
}
if ( ! empty( $values ) ) {
// Add attribute to array, but don't set values.
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_id( $attribute_id );
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
} elseif ( isset( $attribute['options'] ) ) {
// Custom attribute - Add attribute to array and set the values.
if ( is_array( $attribute['options'] ) ) {
$values = $attribute['options'];
} else {
$values = explode( WC_DELIMITER, $attribute['options'] );
}
$attribute_object = new WC_Product_Attribute();
$attribute_object->set_name( $attribute_name );
$attribute_object->set_options( $values );
$attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' );
$attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 );
$attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 );
$attributes[] = $attribute_object;
}
}
$product->set_attributes( $attributes );
}
// Sales and prices.
if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) {
$product->set_regular_price( '' );
$product->set_sale_price( '' );
$product->set_date_on_sale_to( '' );
$product->set_date_on_sale_from( '' );
$product->set_price( '' );
} else {
// Regular Price.
if ( isset( $request['regular_price'] ) ) {
$product->set_regular_price( $request['regular_price'] );
}
// Sale Price.
if ( isset( $request['sale_price'] ) ) {
$product->set_sale_price( $request['sale_price'] );
}
if ( isset( $request['date_on_sale_from'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from'] );
}
if ( isset( $request['date_on_sale_from_gmt'] ) ) {
$product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null );
}
if ( isset( $request['date_on_sale_to'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to'] );
}
if ( isset( $request['date_on_sale_to_gmt'] ) ) {
$product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null );
}
}
// Product parent ID.
if ( isset( $request['parent_id'] ) ) {
$product->set_parent_id( $request['parent_id'] );
}
// Sold individually.
if ( isset( $request['sold_individually'] ) ) {
$product->set_sold_individually( $request['sold_individually'] );
}
// Stock status; stock_status has priority over in_stock.
if ( isset( $request['stock_status'] ) ) {
$stock_status = $request['stock_status'];
} else {
$stock_status = $product->get_stock_status();
}
// Stock data.
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
// Manage stock.
if ( isset( $request['manage_stock'] ) ) {
$product->set_manage_stock( $request['manage_stock'] );
}
// Backorders.
if ( isset( $request['backorders'] ) ) {
$product->set_backorders( $request['backorders'] );
}
if ( $product->is_type( 'grouped' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
} elseif ( $product->is_type( 'external' ) ) {
$product->set_manage_stock( 'no' );
$product->set_backorders( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( 'instock' );
} elseif ( $product->get_manage_stock() ) {
// Stock status is always determined by children so sync later.
if ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Stock quantity.
if ( isset( $request['stock_quantity'] ) ) {
$product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) );
} elseif ( isset( $request['inventory_delta'] ) ) {
$stock_quantity = wc_stock_amount( $product->get_stock_quantity() );
$stock_quantity += wc_stock_amount( $request['inventory_delta'] );
$product->set_stock_quantity( wc_stock_amount( $stock_quantity ) );
}
// Low stock amount.
// isset() returns false for value null, thus we need to check whether the value has been sent by the request.
if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) {
if ( null === $request['low_stock_amount'] ) {
$product->set_low_stock_amount( '' );
} else {
$product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) );
}
}
} else {
// Don't manage stock.
$product->set_manage_stock( 'no' );
$product->set_stock_quantity( '' );
$product->set_stock_status( $stock_status );
$product->set_low_stock_amount( '' );
}
} elseif ( ! $product->is_type( 'variable' ) ) {
$product->set_stock_status( $stock_status );
}
// Upsells.
if ( isset( $request['upsell_ids'] ) ) {
$upsells = array();
$ids = $request['upsell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$upsells[] = $id;
}
}
}
$product->set_upsell_ids( $upsells );
}
// Cross sells.
if ( isset( $request['cross_sell_ids'] ) ) {
$crosssells = array();
$ids = $request['cross_sell_ids'];
if ( ! empty( $ids ) ) {
foreach ( $ids as $id ) {
if ( $id && $id > 0 ) {
$crosssells[] = $id;
}
}
}
$product->set_cross_sell_ids( $crosssells );
}
// Product categories.
if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) {
$product = $this->save_taxonomy_terms( $product, $request['categories'] );
}
// Product tags.
if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) {
$new_tags = array();
foreach ( $request['tags'] as $tag ) {
if ( ! isset( $tag['name'] ) ) {
$new_tags[] = $tag;
continue;
}
if ( ! term_exists( $tag['name'], 'product_tag' ) ) {
// Create the tag if it doesn't exist.
$term = wp_insert_term( $tag['name'], 'product_tag' );
if ( ! is_wp_error( $term ) ) {
$new_tags[] = array(
'id' => $term['term_id'],
);
continue;
}
} else {
// Tag exists, assume user wants to set the product with this tag.
$new_tags[] = array(
'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id,
);
}
}
$product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' );
}
// Downloadable.
if ( isset( $request['downloadable'] ) ) {
$product->set_downloadable( $request['downloadable'] );
}
// Downloadable options.
if ( $product->get_downloadable() ) {
// Downloadable files.
if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) {
$product = $this->save_downloadable_files( $product, $request['downloads'] );
}
// Download limit.
if ( isset( $request['download_limit'] ) ) {
$product->set_download_limit( $request['download_limit'] );
}
// Download expiry.
if ( isset( $request['download_expiry'] ) ) {
$product->set_download_expiry( $request['download_expiry'] );
}
}
// Product url and button text for external products.
if ( $product->is_type( 'external' ) ) {
if ( isset( $request['external_url'] ) ) {
$product->set_product_url( $request['external_url'] );
}
if ( isset( $request['button_text'] ) ) {
$product->set_button_text( $request['button_text'] );
}
}
// Save default attributes for variable products.
if ( $product->is_type( 'variable' ) ) {
$product = $this->save_default_attributes( $product, $request );
}
// Set children for a grouped product.
if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) {
$product->set_children( $request['grouped_products'] );
}
// Check for featured/gallery images, upload it and set it.
if ( isset( $request['images'] ) ) {
$product = $this->set_product_images( $product, $request['images'] );
}
// Allow set meta_data.
if ( is_array( $request['meta_data'] ) ) {
foreach ( $request['meta_data'] as $meta ) {
$product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' );
}
}
if ( ! empty( $request['date_created'] ) ) {
$date = rest_parse_date( $request['date_created'] );
if ( $date ) {
$product->set_date_created( $date );
}
}
if ( ! empty( $request['date_created_gmt'] ) ) {
$date = rest_parse_date( $request['date_created_gmt'], true );
if ( $date ) {
$product->set_date_created( $date );
}
}
/**
* Filters an object before it is inserted via the REST API.
*
* The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug.
*
* @param WC_Data $product Object object.
* @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object.
*/
return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating );
}
/**
* Get the Product's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$weight_unit_label = I18nUtil::get_weight_unit_label( get_option( 'woocommerce_weight_unit', 'kg' ) );
$dimension_unit_label = I18nUtil::get_dimensions_unit_label( get_option( 'woocommerce_dimension_unit', 'cm' ) );
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'slug' => array(
'description' => __( 'Product slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'permalink' => array(
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created' => array(
'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_created_gmt' => array(
'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_modified' => array(
'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Product type.', 'woocommerce' ),
'type' => 'string',
'default' => 'simple',
'enum' => array_keys( wc_get_product_types() ),
'context' => array( 'view', 'edit' ),
),
'status' => array(
'description' => __( 'Product status (post status).', 'woocommerce' ),
'type' => 'string',
'default' => 'publish',
'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future', 'auto-draft', 'trash' ) ),
'context' => array( 'view', 'edit' ),
),
'featured' => array(
'description' => __( 'Featured product.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'catalog_visibility' => array(
'description' => __( 'Catalog visibility.', 'woocommerce' ),
'type' => 'string',
'default' => 'visible',
'enum' => array( 'visible', 'catalog', 'search', 'hidden' ),
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'Product description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'short_description' => array(
'description' => __( 'Product short description.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sku' => array(
'description' => __( 'Unique identifier.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'price' => array(
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'regular_price' => array(
'description' => __( 'Product regular price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sale_price' => array(
'description' => __( 'Product sale price.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from' => array(
'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_from_gmt' => array(
'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'date_on_sale_to_gmt' => array(
'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
),
'price_html' => array(
'description' => __( 'Price formatted in HTML.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'on_sale' => array(
'description' => __( 'Shows if the product is on sale.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'purchasable' => array(
'description' => __( 'Shows if the product can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_sales' => array(
'description' => __( 'Amount of sales.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'virtual' => array(
'description' => __( 'If the product is virtual.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloadable' => array(
'description' => __( 'If the product is downloadable.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'downloads' => array(
'description' => __( 'List of downloadable files.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'File ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'File name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'file' => array(
'description' => __( 'File URL.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'download_limit' => array(
'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'download_expiry' => array(
'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ),
'type' => 'integer',
'default' => -1,
'context' => array( 'view', 'edit' ),
),
'external_url' => array(
'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'button_text' => array(
'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'tax_status' => array(
'description' => __( 'Tax status.', 'woocommerce' ),
'type' => 'string',
'default' => 'taxable',
'enum' => array( 'taxable', 'shipping', 'none' ),
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
'description' => __( 'Tax class.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'manage_stock' => array(
'description' => __( 'Stock management at product level.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'stock_quantity' => array(
'description' => __( 'Stock quantity.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'stock_status' => array(
'description' => __( 'Controls the stock status of the product.', 'woocommerce' ),
'type' => 'string',
'default' => 'instock',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'context' => array( 'view', 'edit' ),
),
'backorders' => array(
'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ),
'type' => 'string',
'default' => 'no',
'enum' => array( 'no', 'notify', 'yes' ),
'context' => array( 'view', 'edit' ),
),
'backorders_allowed' => array(
'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'backordered' => array(
'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the product.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ),
),
'sold_individually' => array(
'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'weight' => array(
/* translators: %s: weight unit */
'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'dimensions' => array(
'description' => __( 'Product dimensions.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'properties' => array(
'length' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'width' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'height' => array(
/* translators: %s: dimension unit */
'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit_label ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
'shipping_required' => array(
'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_taxable' => array(
'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_class' => array(
'description' => __( 'Shipping class slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'shipping_class_id' => array(
'description' => __( 'Shipping class ID.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'reviews_allowed' => array(
'description' => __( 'Allow reviews.', 'woocommerce' ),
'type' => 'boolean',
'default' => true,
'context' => array( 'view', 'edit' ),
),
'post_password' => array(
'description' => __( 'Post password.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'average_rating' => array(
'description' => __( 'Reviews average rating.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'rating_count' => array(
'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'related_ids' => array(
'description' => __( 'List of related products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'upsell_ids' => array(
'description' => __( 'List of up-sell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'cross_sell_ids' => array(
'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
),
'parent_id' => array(
'description' => __( 'Product parent ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'purchase_note' => array(
'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'categories' => array(
'description' => __( 'List of categories.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Category ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Category name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Category slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'tags' => array(
'description' => __( 'List of tags.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Tag ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Tag name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'slug' => array(
'description' => __( 'Tag slug.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
),
),
'images' => array(
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Image ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'date_created' => array(
'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_created_gmt' => array(
'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified' => array(
'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_modified_gmt' => array(
'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'src' => array(
'description' => __( 'Image URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Image name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'alt' => array(
'description' => __( 'Image alternative text.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'has_options' => array(
'description' => __( 'Shows if the product needs to be configured before it can be bought.', 'woocommerce' ),
'type' => 'boolean',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'attributes' => array(
'description' => __( 'List of attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'position' => array(
'description' => __( 'Attribute position.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'visible' => array(
'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'variation' => array(
'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'context' => array( 'view', 'edit' ),
),
'options' => array(
'description' => __( 'List of available term names of the attribute.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
),
),
),
),
'default_attributes' => array(
'description' => __( 'Defaults variation attributes.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Attribute ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'name' => array(
'description' => __( 'Attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'option' => array(
'description' => __( 'Selected attribute term name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
),
),
'variations' => array(
'description' => __( 'List of variations IDs.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'integer',
),
'readonly' => true,
),
'grouped_products' => array(
'description' => __( 'List of grouped products ID.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'menu_order' => array(
'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
),
'meta_data' => array(
'description' => __( 'Meta data.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Meta ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'key' => array(
'description' => __( 'Meta key.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
),
),
),
);
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$schema['properties']['permalink_template'] = array(
'description' => __( 'Permalink template for the product.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
$schema['properties']['generated_slug'] = array(
'description' => __( 'Slug automatically generated from the product name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'edit' ),
'readonly' => true,
);
}
return $this->add_additional_fields_schema( $schema );
}
/**
* Add new options for 'orderby' to the collection params.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) );
unset( $params['in_stock'] );
$params['stock_status'] = array(
'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ),
'type' => 'string',
'enum' => array_keys( wc_get_product_stock_status_options() ),
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['search_sku'] = array(
'description' => __( 'Limit results to those with a SKU that partial matches a string.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get product data.
*
* @param WC_Product $product Product instance.
* @param string $context Request context. Options: 'view' and 'edit'.
*
* @return array
*/
protected function get_product_data( $product, $context = 'view' ) {
$data = parent::get_product_data( ...func_get_args() );
if ( isset( $this->request ) ) {
$fields = $this->get_fields_for_response( $this->request );
// Add stock_status if needed.
if ( in_array( 'stock_status', $fields, true ) ) {
$data['stock_status'] = $product->get_stock_status( $context );
}
// Add has_options if needed.
if ( in_array( 'has_options', $fields, true ) ) {
$data['has_options'] = $product->has_options( $context );
}
if ( in_array( 'post_password', $fields, true ) ) {
$data['post_password'] = $product->get_post_password( $context );
}
$post_type_obj = get_post_type_object( $this->post_type );
if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
$permalink_template_requested = in_array( 'permalink_template', $fields, true );
$generated_slug_requested = in_array( 'generated_slug', $fields, true );
if ( $permalink_template_requested || $generated_slug_requested ) {
if ( ! function_exists( 'get_sample_permalink' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$sample_permalink = get_sample_permalink( $product->get_id(), $product->get_name(), '' );
// Add permalink_template if needed.
if ( $permalink_template_requested ) {
$data['permalink_template'] = $sample_permalink[0];
}
// Add generated_slug if needed.
if ( $generated_slug_requested ) {
$data['generated_slug'] = $sample_permalink[1];
}
}
}
}
return $data;
}
}
Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php 0000644 00000006715 15154275247 0022471 0 ustar 00 <?php
/**
* REST API Reports Coupons Totals controller
*
* Handles requests to the /reports/coupons/count endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Coupons Totals controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_Controller
*/
class WC_REST_Report_Coupons_Totals_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/coupons/totals';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
global $wpdb;
$data = get_transient( 'rest_api_coupons_type_count' );
if ( false !== $data ) {
return $data;
}
$types = wc_get_coupon_types();
$data = array();
foreach ( $types as $slug => $name ) {
$results = $wpdb->get_results(
$wpdb->prepare( "
SELECT count(meta_id) AS total
FROM $wpdb->postmeta
WHERE meta_key = 'discount_type'
AND meta_value = %s
", $slug )
);
$total = isset( $results[0] ) ? (int) $results[0]->total : 0;
$data[] = array(
'slug' => $slug,
'name' => $name,
'total' => $total,
);
}
set_transient( 'rest_api_coupons_type_count', $data, YEAR_IN_SECONDS );
return $data;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'name' => $report->name,
'total' => $report->total,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_coupons_count', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_coupon_total',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Coupon type name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Amount of coupons.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-report-customers-totals-controller.php 0000644 00000007451 15154275247 0023025 0 ustar 00 <?php
/**
* REST API Reports Customers Totals controller
*
* Handles requests to the /reports/customers/count endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Customers Totals controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_Controller
*/
class WC_REST_Report_Customers_Totals_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/customers/totals';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
$users_count = count_users();
$total_customers = 0;
foreach ( $users_count['avail_roles'] as $role => $total ) {
if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) {
continue;
}
$total_customers += (int) $total;
}
$customers_query = new WP_User_Query(
array(
'role__not_in' => array( 'administrator', 'shop_manager' ),
'number' => 0,
'fields' => 'ID',
'count_total' => true,
'meta_query' => array( // WPCS: slow query ok.
array(
'key' => 'paying_customer',
'value' => 1,
'compare' => '=',
),
),
)
);
$total_paying = (int) $customers_query->get_total();
$data = array(
array(
'slug' => 'paying',
'name' => __( 'Paying customer', 'woocommerce' ),
'total' => $total_paying,
),
array(
'slug' => 'non_paying',
'name' => __( 'Non-paying customer', 'woocommerce' ),
'total' => $total_customers - $total_paying,
),
);
return $data;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'name' => $report->name,
'total' => $report->total,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_customers_count', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customer_total',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Customer type name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Amount of customers.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-report-orders-totals-controller.php 0000644 00000006160 15154275247 0022273 0 ustar 00 <?php
/**
* REST API Reports Orders Totals controller
*
* Handles requests to the /reports/orders/count endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Orders Totals controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_Controller
*/
class WC_REST_Report_Orders_Totals_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/orders/totals';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
$totals = wp_count_posts( 'shop_order' );
$data = array();
foreach ( wc_get_order_statuses() as $slug => $name ) {
if ( ! isset( $totals->$slug ) ) {
continue;
}
$data[] = array(
'slug' => str_replace( 'wc-', '', $slug ),
'name' => $name,
'total' => (int) $totals->$slug,
);
}
return $data;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'name' => $report->name,
'total' => $report->total,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_orders_count', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_order_total',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Order status name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Amount of orders.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-report-products-totals-controller.php 0000644 00000006356 15154275247 0022647 0 ustar 00 <?php
/**
* REST API Reports Products Totals controller
*
* Handles requests to the /reports/products/count endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Products Totals controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_Controller
*/
class WC_REST_Report_Products_Totals_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/products/totals';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
$types = wc_get_product_types();
$terms = get_terms(
array(
'taxonomy' => 'product_type',
'hide_empty' => false,
)
);
$data = array();
foreach ( $terms as $product_type ) {
if ( ! isset( $types[ $product_type->name ] ) ) {
continue;
}
$data[] = array(
'slug' => $product_type->name,
'name' => $types[ $product_type->name ],
'total' => (int) $product_type->count,
);
}
return $data;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'name' => $report->name,
'total' => $report->total,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_products_count', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_product_total',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Product type name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Amount of products.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php 0000644 00000006470 15154275247 0022465 0 ustar 00 <?php
/**
* REST API Reports Reviews Totals controller
*
* Handles requests to the /reports/reviews/count endpoint.
*
* @package WooCommerce\RestApi
* @since 3.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports Reviews Totals controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_Controller
*/
class WC_REST_Report_Reviews_Totals_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/reviews/totals';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
$data = array();
$query_data = array(
'count' => true,
'post_type' => 'product',
'meta_key' => 'rating', // WPCS: slow query ok.
'meta_value' => '', // WPCS: slow query ok.
);
for ( $i = 1; $i <= 5; $i++ ) {
$query_data['meta_value'] = $i;
$data[] = array(
'slug' => 'rated_' . $i . '_out_of_5',
/* translators: %s: average rating */
'name' => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ),
'total' => (int) get_comments( $query_data ),
);
}
return $data;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'name' => $report->name,
'total' => $report->total,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_reviews_count', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_review_total',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'name' => array(
'description' => __( 'Review type name.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'total' => array(
'description' => __( 'Amount of reviews.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-report-sales-controller.php 0000644 00000000752 15154275247 0020601 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/sales endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Report Sales controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Report_Sales_V2_Controller
*/
class WC_REST_Report_Sales_Controller extends WC_REST_Report_Sales_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-report-top-sellers-controller.php 0000644 00000001010 15154275247 0021727 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports/top_sellers endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Report Top Sellers controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Report_Top_Sellers_V2_Controller
*/
class WC_REST_Report_Top_Sellers_Controller extends WC_REST_Report_Top_Sellers_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-reports-controller.php 0000644 00000003152 15154275247 0017654 0 ustar 00 <?php
/**
* REST API Reports controller
*
* Handles requests to the reports endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Reports_V2_Controller
*/
class WC_REST_Reports_Controller extends WC_REST_Reports_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Get reports list.
*
* @since 3.5.0
* @return array
*/
protected function get_reports() {
$reports = parent::get_reports();
$reports[] = array(
'slug' => 'orders/totals',
'description' => __( 'Orders totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'products/totals',
'description' => __( 'Products totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'customers/totals',
'description' => __( 'Customers totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'coupons/totals',
'description' => __( 'Coupons totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'reviews/totals',
'description' => __( 'Reviews totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'categories/totals',
'description' => __( 'Categories totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'tags/totals',
'description' => __( 'Tags totals.', 'woocommerce' ),
);
$reports[] = array(
'slug' => 'attributes/totals',
'description' => __( 'Attributes totals.', 'woocommerce' ),
);
return $reports;
}
}
Controllers/Version3/class-wc-rest-setting-options-controller.php 0000644 00000016613 15154275247 0021332 0 ustar 00 <?php
/**
* REST API Setting Options controller
*
* Handles requests to the /settings/$group/$setting endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Setting Options controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Setting_Options_V2_Controller
*/
class WC_REST_Setting_Options_Controller extends WC_REST_Setting_Options_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Get setting data.
*
* @param string $group_id Group ID.
* @param string $setting_id Setting ID.
* @return stdClass|WP_Error
*/
public function get_setting( $group_id, $setting_id ) {
$setting = parent::get_setting( $group_id, $setting_id );
if ( is_wp_error( $setting ) ) {
return $setting;
}
$setting['group_id'] = $group_id;
return $setting;
}
/**
* Callback for allowed keys for each setting response.
*
* @param string $key Key to check.
* @return boolean
*/
public function allowed_setting_keys( $key ) {
return in_array(
$key, array(
'id',
'group_id',
'label',
'description',
'default',
'tip',
'placeholder',
'type',
'options',
'value',
'option_key',
), true
);
}
/**
* Get all settings in a group.
*
* @param string $group_id Group ID.
* @return array|WP_Error
*/
public function get_group_settings( $group_id ) {
if ( empty( $group_id ) ) {
return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
}
$settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
if ( empty( $settings ) ) {
return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) );
}
$filtered_settings = array();
foreach ( $settings as $setting ) {
$option_key = $setting['option_key'];
$setting = $this->filter_setting( $setting );
$default = isset( $setting['default'] ) ? $setting['default'] : '';
// Get the option value.
if ( is_array( $option_key ) ) {
$option = get_option( $option_key[0] );
$setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default;
} else {
$admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default );
$setting['value'] = $admin_setting_value;
}
if ( 'multi_select_countries' === $setting['type'] ) {
$setting['options'] = WC()->countries->get_countries();
$setting['type'] = 'multiselect';
} elseif ( 'single_select_country' === $setting['type'] ) {
$setting['type'] = 'select';
$setting['options'] = $this->get_countries_and_states();
} elseif ( $setting['type'] === 'single_select_page' || $setting['type'] === 'single_select_page_with_search' ) {
$pages = get_pages(
array(
'sort_column' => 'menu_order',
'sort_order' => 'ASC',
'hierarchical' => 0,
)
);
$options = array();
foreach ( $pages as $page ) {
$options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID;
}
$setting['type'] = 'select';
$setting['options'] = $options;
}
$filtered_settings[] = $setting;
}
return $filtered_settings;
}
/**
* Returns a list of countries and states for use in the base location setting.
*
* @since 3.0.7
* @return array Array of states and countries.
*/
private function get_countries_and_states() {
$countries = WC()->countries->get_countries();
if ( ! $countries ) {
return array();
}
$output = array();
foreach ( $countries as $key => $value ) {
$states = WC()->countries->get_states( $key );
if ( $states ) {
foreach ( $states as $state_key => $state_value ) {
$output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value;
}
} else {
$output[ $key ] = $value;
}
}
return $output;
}
/**
* Get the settings schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'setting',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier for the setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'group_id' => array(
'description' => __( 'An identifier for the group this setting belongs to.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_title',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'tip' => array(
'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'placeholder' => array(
'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'type' => array(
'description' => __( 'Type of setting.', 'woocommerce' ),
'type' => 'string',
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'context' => array( 'view', 'edit' ),
'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ),
'readonly' => true,
),
'options' => array(
'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-settings-controller.php 0000644 00000006172 15154275247 0020023 0 ustar 00 <?php
/**
* REST API Settings controller
*
* Handles requests to the /settings endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Settings controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Settings_V2_Controller
*/
class WC_REST_Settings_Controller extends WC_REST_Settings_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Register routes.
*/
public function register_routes() {
parent::register_routes();
register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'update_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
) );
}
/**
* Makes sure the current user has access to WRITE the settings APIs.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_Error|bool
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Update a setting.
*
* @param WP_REST_Request $request Request data.
* @return WP_Error|WP_REST_Response
*/
public function update_item( $request ) {
$options_controller = new WC_REST_Setting_Options_Controller();
$response = $options_controller->update_item( $request );
return $response;
}
/**
* Get the groups schema, conforming to JSON Schema.
*
* @since 3.0.0
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'setting_group',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'label' => array(
'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'description' => array(
'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'parent_id' => array(
'description' => __( 'ID of parent grouping.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
'sub_groups' => array(
'description' => __( 'IDs for settings sub groups.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-shipping-methods-controller.php 0000644 00000001001 15154275247 0021427 0 ustar 00 <?php
/**
* REST API WC Shipping Methods controller
*
* Handles requests to the /shipping_methods endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Shipping methods controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Methods_V2_Controller
*/
class WC_REST_Shipping_Methods_Controller extends WC_REST_Shipping_Methods_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php 0000644 00000001054 15154275247 0022740 0 ustar 00 <?php
/**
* REST API Shipping Zone Locations controller
*
* Handles requests to the /shipping/zones/<id>/locations endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zone Locations class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zone_Locations_V2_Controller
*/
class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zone_Locations_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php 0000644 00000002100 15154275247 0022401 0 ustar 00 <?php
/**
* REST API Shipping Zone Methods controller
*
* Handles requests to the /shipping/zones/<id>/methods endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zone Methods class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zone_Methods_V2_Controller
*/
class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Get the settings schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
// Get parent schema to append additional supported settings types for shipping zone method.
$schema = parent::get_item_schema();
// Append additional settings supported types (class, order).
$schema['properties']['settings']['properties']['type']['enum'][] = 'class';
$schema['properties']['settings']['properties']['type']['enum'][] = 'order';
return $this->add_additional_fields_schema( $schema );
}
}
Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php 0000644 00000007254 15154275247 0022052 0 ustar 00 <?php
/**
* REST API Shipping Zones Controller base
*
* Houses common functionality between Shipping Zones and Locations.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* REST API Shipping Zones base class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Controller
*/
abstract class WC_REST_Shipping_Zones_Controller_Base extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'shipping/zones';
/**
* Retrieve a Shipping Zone by it's ID.
*
* @param int $zone_id Shipping Zone ID.
* @return WC_Shipping_Zone|WP_Error
*/
protected function get_zone( $zone_id ) {
$zone = WC_Shipping_Zones::get_zone_by( 'zone_id', $zone_id );
if ( false === $zone ) {
return new WP_Error( 'woocommerce_rest_shipping_zone_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
return $zone;
}
/**
* Check whether a given request has permission to read Shipping Zones.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_shipping_enabled() ) {
return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create Shipping Zones.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
if ( ! wc_shipping_enabled() ) {
return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to edit Shipping Zones.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_items_permissions_check( $request ) {
if ( ! wc_shipping_enabled() ) {
return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check whether a given request has permission to delete Shipping Zones.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_items_permissions_check( $request ) {
if ( ! wc_shipping_enabled() ) {
return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) );
}
if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
}
Controllers/Version3/class-wc-rest-shipping-zones-controller.php 0000644 00000000760 15154275247 0021135 0 ustar 00 <?php
/**
* REST API Shipping Zones controller
*
* Handles requests to the /shipping/zones endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Shipping Zones class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Shipping_Zones_V2_Controller
*/
class WC_REST_Shipping_Zones_Controller extends WC_REST_Shipping_Zones_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-system-status-controller.php 0000644 00000000757 15154275247 0021033 0 ustar 00 <?php
/**
* REST API WC System Status controller
*
* Handles requests to the /system_status endpoint.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* System status controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_System_Status_V2_Controller
*/
class WC_REST_System_Status_Controller extends WC_REST_System_Status_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-system-status-tools-controller.php 0000644 00000001020 15154275247 0022151 0 ustar 00 <?php
/**
* REST API WC System Status Tools Controller
*
* Handles requests to the /system_status/tools/* endpoints.
*
* @package WooCommerce\RestApi
* @since 3.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* System status tools controller.
*
* @package WooCommerce\RestApi
* @extends WC_REST_System_Status_Tools_V2_Controller
*/
class WC_REST_System_Status_Tools_Controller extends WC_REST_System_Status_Tools_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-tax-classes-controller.php 0000644 00000000753 15154275247 0020411 0 ustar 00 <?php
/**
* REST API Tax Classes controller
*
* Handles requests to the /taxes/classes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Tax Classes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Tax_Classes_V2_Controller
*/
class WC_REST_Tax_Classes_Controller extends WC_REST_Tax_Classes_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
}
Controllers/Version3/class-wc-rest-taxes-controller.php 0000644 00000007053 15154275247 0017306 0 ustar 00 <?php
/**
* REST API Taxes controller
*
* Handles requests to the /taxes endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Taxes controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Taxes_V2_Controller
*/
class WC_REST_Taxes_Controller extends WC_REST_Taxes_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Add tax rate locales to the response array.
*
* @param array $data Response data.
* @param stdClass $tax Tax object.
*
* @return array
*/
protected function add_tax_rate_locales( $data, $tax ) {
global $wpdb;
$data = parent::add_tax_rate_locales( $data, $tax );
$data['postcodes'] = array();
$data['cities'] = array();
// Get locales from a tax rate.
$locales = $wpdb->get_results(
$wpdb->prepare(
"
SELECT location_code, location_type
FROM {$wpdb->prefix}woocommerce_tax_rate_locations
WHERE tax_rate_id = %d
",
$tax->tax_rate_id
)
);
if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) {
foreach ( $locales as $locale ) {
if ( 'postcode' === $locale->location_type ) {
$data['postcodes'][] = $locale->location_code;
} elseif ( 'city' === $locale->location_type ) {
$data['cities'][] = $locale->location_code;
}
}
}
return $data;
}
/**
* Get the taxes schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['postcodes'] = array(
'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['cities'] = array(
'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'string',
),
'context' => array( 'view', 'edit' ),
);
$schema['properties']['postcode']['description'] =
__( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' );
$schema['properties']['city']['description'] =
__( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' );
return $schema;
}
/**
* Create a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response The response, or an error.
*/
public function create_item( $request ) {
$this->adjust_cities_and_postcodes( $request );
return parent::create_item( $request );
}
/**
* Update a single tax.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response The response, or an error.
*/
public function update_item( $request ) {
$this->adjust_cities_and_postcodes( $request );
return parent::update_item( $request );
}
/**
* Convert array "cities" and "postcodes" parameters
* into semicolon-separated strings "city" and "postcode".
*
* @param WP_REST_Request $request The request to adjust.
*/
private function adjust_cities_and_postcodes( &$request ) {
if ( isset( $request['cities'] ) ) {
$request['city'] = join( ';', $request['cities'] );
}
if ( isset( $request['postcodes'] ) ) {
$request['postcode'] = join( ';', $request['postcodes'] );
}
}
}
Controllers/Version3/class-wc-rest-terms-controller.php 0000644 00000062737 15154275247 0017326 0 ustar 00 <?php
/**
* Abstract Rest Terms Controller
*
* @package WooCommerce\RestApi
* @version 3.3.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use Automattic\WooCommerce\Internal\AssignDefaultCategory;
/**
* Terms controller class.
*/
abstract class WC_REST_Terms_Controller extends WC_REST_Controller {
/**
* Route base.
*
* @var string
*/
protected $rest_base = '';
/**
* Taxonomy.
*
* @var string
*/
protected $taxonomy = '';
/**
* Cached taxonomies by attribute id.
*
* @var array
*/
protected $taxonomies_by_id = array();
/**
* Register the routes for terms.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
array(
'name' => array(
'type' => 'string',
'description' => __( 'Name for the resource.', 'woocommerce' ),
'required' => true,
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'id' => array(
'description' => __( 'Unique identifier for the resource.', 'woocommerce' ),
'type' => 'integer',
),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
'type' => 'boolean',
'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ),
),
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/batch',
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'batch_items' ),
'permission_callback' => array( $this, 'batch_items_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
),
'schema' => array( $this, 'get_public_batch_schema' ),
)
);
}
/**
* Check if a given request has access to read the terms.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'read' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to create a term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_item_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'create' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to read a term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'read' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to update a term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'edit' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access to delete a term.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function delete_item_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'delete' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check if a given request has access batch create, update and delete items.
*
* @param WP_REST_Request $request Full details about the request.
* @return boolean|WP_Error
*/
public function batch_items_permissions_check( $request ) {
$permissions = $this->check_permissions( $request, 'batch' );
if ( is_wp_error( $permissions ) ) {
return $permissions;
}
if ( ! $permissions ) {
return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Check permissions.
*
* @param WP_REST_Request $request Full details about the request.
* @param string $context Request context.
* @return bool|WP_Error
*/
protected function check_permissions( $request, $context = 'read' ) {
// Get taxonomy.
$taxonomy = $this->get_taxonomy( $request );
if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
// Check permissions for a single term.
$id = intval( $request['id'] );
if ( $id ) {
$term = get_term( $id, $taxonomy );
if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) {
return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) );
}
return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id );
}
return wc_rest_check_product_term_permissions( $taxonomy, $context );
}
/**
* Get terms associated with a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function get_items( $request ) {
$taxonomy = $this->get_taxonomy( $request );
$prepared_args = array(
'exclude' => $request['exclude'],
'include' => $request['include'],
'order' => $request['order'],
'orderby' => $request['orderby'],
'product' => $request['product'],
'hide_empty' => $request['hide_empty'],
'number' => $request['per_page'],
'search' => $request['search'],
'slug' => $request['slug'],
);
if ( ! empty( $request['offset'] ) ) {
$prepared_args['offset'] = $request['offset'];
} else {
$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
}
$taxonomy_obj = get_taxonomy( $taxonomy );
if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) {
if ( 0 === $request['parent'] ) {
// Only query top-level terms.
$prepared_args['parent'] = 0;
} else {
if ( $request['parent'] ) {
$prepared_args['parent'] = $request['parent'];
}
}
}
/**
* Filter the query arguments, before passing them to `get_terms()`.
*
* Enables adding extra arguments or setting defaults for a terms
* collection request.
*
* @see https://developer.wordpress.org/reference/functions/get_terms/
*
* @param array $prepared_args Array of arguments to be
* passed to get_terms.
* @param WP_REST_Request $request The current request.
*/
$prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request );
if ( ! empty( $prepared_args['product'] ) ) {
$query_result = $this->get_terms_for_product( $prepared_args, $request );
$total_terms = $this->total_terms;
} else {
$query_result = get_terms( $taxonomy, $prepared_args );
$count_args = $prepared_args;
unset( $count_args['number'] );
unset( $count_args['offset'] );
$total_terms = wp_count_terms( $taxonomy, $count_args );
// Ensure we don't return results when offset is out of bounds.
// See https://core.trac.wordpress.org/ticket/35935.
if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) {
$query_result = array();
}
// wp_count_terms can return a falsy value when the term has no children.
if ( ! $total_terms ) {
$total_terms = 0;
}
}
$response = array();
foreach ( $query_result as $term ) {
$data = $this->prepare_item_for_response( $term, $request );
$response[] = $this->prepare_response_for_collection( $data );
}
$response = rest_ensure_response( $response );
// Store pagination values for headers then unset for count query.
$per_page = (int) $prepared_args['number'];
$page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );
$response->header( 'X-WP-Total', (int) $total_terms );
$max_pages = ceil( $total_terms / $per_page );
$response->header( 'X-WP-TotalPages', (int) $max_pages );
$base = str_replace( '(?P<attribute_id>[\d]+)', $request['attribute_id'] ?? '', $this->rest_base );
$base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Create a single term for a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function create_item( $request ) {
$taxonomy = $this->get_taxonomy( $request );
$name = $request['name'];
$args = array();
$schema = $this->get_item_schema();
if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
$args['description'] = $request['description'];
}
if ( isset( $request['slug'] ) ) {
$args['slug'] = $request['slug'];
}
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
}
$args['parent'] = $request['parent'];
}
$term = wp_insert_term( $name, $taxonomy, $args );
if ( is_wp_error( $term ) ) {
$error_data = array( 'status' => 400 );
// If we're going to inform the client that the term exists,
// give them the identifier they can actually use.
$term_id = $term->get_error_data( 'term_exists' );
if ( $term_id ) {
$error_data['resource_id'] = $term_id;
}
return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data );
}
$term = get_term( $term['term_id'], $taxonomy );
$this->update_additional_fields_for_object( $term, $request );
// Add term data.
$meta_fields = $this->update_term_meta_fields( $term, $request );
if ( is_wp_error( $meta_fields ) ) {
wp_delete_term( $term->term_id, $taxonomy );
return $meta_fields;
}
/**
* Fires after a single term is created or updated via the REST API.
*
* @param WP_Term $term Inserted Term object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating term, false when updating.
*/
do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $term, $request );
$response = rest_ensure_response( $response );
$response->set_status( 201 );
$base = '/' . $this->namespace . '/' . $this->rest_base;
if ( ! empty( $request['attribute_id'] ) ) {
$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
}
$response->header( 'Location', rest_url( $base . '/' . $term->term_id ) );
return $response;
}
/**
* Get a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function get_item( $request ) {
$taxonomy = $this->get_taxonomy( $request );
$term = get_term( (int) $request['id'], $taxonomy );
if ( is_wp_error( $term ) ) {
return $term;
}
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Update a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Request|WP_Error
*/
public function update_item( $request ) {
$taxonomy = $this->get_taxonomy( $request );
$term = get_term( (int) $request['id'], $taxonomy );
$schema = $this->get_item_schema();
$prepared_args = array();
if ( isset( $request['name'] ) ) {
$prepared_args['name'] = $request['name'];
}
if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
$prepared_args['description'] = $request['description'];
}
if ( isset( $request['slug'] ) ) {
$prepared_args['slug'] = $request['slug'];
}
if ( isset( $request['parent'] ) ) {
if ( ! is_taxonomy_hierarchical( $taxonomy ) ) {
return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) );
}
$prepared_args['parent'] = $request['parent'];
}
// Only update the term if we haz something to update.
if ( ! empty( $prepared_args ) ) {
$update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args );
if ( is_wp_error( $update ) ) {
return $update;
}
}
$term = get_term( (int) $request['id'], $taxonomy );
$this->update_additional_fields_for_object( $term, $request );
// Update term data.
$meta_fields = $this->update_term_meta_fields( $term, $request );
if ( is_wp_error( $meta_fields ) ) {
return $meta_fields;
}
/**
* Fires after a single term is created or updated via the REST API.
*
* @param WP_Term $term Inserted Term object.
* @param WP_REST_Request $request Request object.
* @param boolean $creating True when creating term, false when updating.
*/
do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false );
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $term, $request );
return rest_ensure_response( $response );
}
/**
* Delete a single term from a taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function delete_item( $request ) {
$taxonomy = $this->get_taxonomy( $request );
$force = isset( $request['force'] ) ? (bool) $request['force'] : false;
// We don't support trashing for this type, error out.
if ( ! $force ) {
return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) );
}
$term = get_term( (int) $request['id'], $taxonomy );
// Get default category id.
$default_category_id = absint( get_option( 'default_product_cat', 0 ) );
// Prevent deleting the default product category.
if ( $default_category_id === (int) $request['id'] ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
$request->set_param( 'context', 'edit' );
$response = $this->prepare_item_for_response( $term, $request );
$retval = wp_delete_term( $term->term_id, $term->taxonomy );
if ( ! $retval ) {
return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) );
}
// Schedule action to assign default category.
wc_get_container()->get( AssignDefaultCategory::class )->schedule_action();
/**
* Fires after a single term is deleted via the REST API.
*
* @param WP_Term $term The deleted term.
* @param WP_REST_Response $response The response data.
* @param WP_REST_Request $request The request sent to the API.
*/
do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request );
return $response;
}
/**
* Prepare links for the request.
*
* @param object $term Term object.
* @param WP_REST_Request $request Full details about the request.
* @return array Links for the given term.
*/
protected function prepare_links( $term, $request ) {
$base = '/' . $this->namespace . '/' . $this->rest_base;
if ( ! empty( $request['attribute_id'] ) ) {
$base = str_replace( '(?P<attribute_id>[\d]+)', (int) $request['attribute_id'], $base );
}
$links = array(
'self' => array(
'href' => rest_url( trailingslashit( $base ) . $term->term_id ),
),
'collection' => array(
'href' => rest_url( $base ),
),
);
if ( $term->parent ) {
$parent_term = get_term( (int) $term->parent, $term->taxonomy );
if ( $parent_term ) {
$links['up'] = array(
'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ),
);
}
}
return $links;
}
/**
* Update term meta fields.
*
* @param WP_Term $term Term object.
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
protected function update_term_meta_fields( $term, $request ) {
return true;
}
/**
* Get the terms attached to a product.
*
* This is an alternative to `get_terms()` that uses `get_the_terms()`
* instead, which hits the object cache. There are a few things not
* supported, notably `include`, `exclude`. In `self::get_items()` these
* are instead treated as a full query.
*
* @param array $prepared_args Arguments for `get_terms()`.
* @param WP_REST_Request $request Full details about the request.
* @return array List of term objects. (Total count in `$this->total_terms`).
*/
protected function get_terms_for_product( $prepared_args, $request ) {
$taxonomy = $this->get_taxonomy( $request );
$query_result = get_the_terms( $prepared_args['product'], $taxonomy );
if ( empty( $query_result ) ) {
$this->total_terms = 0;
return array();
}
// get_items() verifies that we don't have `include` set, and default.
// ordering is by `name`.
if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) {
switch ( $prepared_args['orderby'] ) {
case 'id':
$this->sort_column = 'term_id';
break;
case 'slug':
case 'term_group':
case 'description':
case 'count':
$this->sort_column = $prepared_args['orderby'];
break;
}
usort( $query_result, array( $this, 'compare_terms' ) );
}
if ( strtolower( $prepared_args['order'] ) !== 'asc' ) {
$query_result = array_reverse( $query_result );
}
// Pagination.
$this->total_terms = count( $query_result );
$query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] );
return $query_result;
}
/**
* Comparison function for sorting terms by a column.
*
* Uses `$this->sort_column` to determine field to sort by.
*
* @param stdClass $left Term object.
* @param stdClass $right Term object.
* @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left.
*/
protected function compare_terms( $left, $right ) {
$col = $this->sort_column;
$left_val = $left->$col;
$right_val = $right->$col;
if ( is_int( $left_val ) && is_int( $right_val ) ) {
return $left_val - $right_val;
}
return strcmp( $left_val, $right_val );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['context']['default'] = 'view';
$params['exclude'] = array(
'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['include'] = array(
'description' => __( 'Limit result set to specific ids.', 'woocommerce' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
$params['offset'] = array(
'description' => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => 'asc',
'enum' => array(
'asc',
'desc',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => 'name',
'enum' => array(
'id',
'include',
'name',
'slug',
'term_group',
'description',
'count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['hide_empty'] = array(
'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'validate_callback' => 'rest_validate_request_arg',
);
$params['parent'] = array(
'description' => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['product'] = array(
'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ),
'type' => 'integer',
'default' => null,
'validate_callback' => 'rest_validate_request_arg',
);
$params['slug'] = array(
'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
/**
* Get taxonomy.
*
* @param WP_REST_Request $request Full details about the request.
* @return int|WP_Error
*/
protected function get_taxonomy( $request ) {
$attribute_id = $request['attribute_id'];
if ( empty( $attribute_id ) ) {
return $this->taxonomy;
}
if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) {
return $this->taxonomies_by_id[ $attribute_id ];
}
$taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] );
if ( ! empty( $taxonomy ) ) {
$this->taxonomy = $taxonomy;
$this->taxonomies_by_id[ $attribute_id ] = $taxonomy;
}
return $taxonomy;
}
}
Controllers/Version3/class-wc-rest-webhooks-controller.php 0000644 00000001172 15154275247 0017777 0 ustar 00 <?php
/**
* REST API Webhooks controller
*
* Handles requests to the /webhooks endpoint.
*
* @package WooCommerce\RestApi
* @since 2.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Webhooks controller class.
*
* @package WooCommerce\RestApi
* @extends WC_REST_Webhooks_V2_Controller
*/
class WC_REST_Webhooks_Controller extends WC_REST_Webhooks_V2_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Get the default REST API version.
*
* @since 3.0.0
* @return string
*/
protected function get_default_api_version() {
return 'wp_api_v3';
}
}
Package.php 0000644 00000003027 15154275247 0006626 0 ustar 00 <?php
/**
* Deprecated notice: This class is deprecated as of version 4.5.0. WooCommerce API is now part of core and not packaged separately.
*
* Returns information about the package and handles init.
*
* @package WooCommerce\RestApi
*/
namespace Automattic\WooCommerce\RestApi;
defined( 'ABSPATH' ) || exit;
/**
* Main package class.
*
* @deprecated Use \Automattic\WooCommerce\RestApi\Server directly.
*/
class Package {
/**
* Version.
*
* @deprecated since 4.5.0. This tracks WooCommerce version now.
* @var string
*/
const VERSION = WC_VERSION;
/**
* Init the package - load the REST API Server class.
*
* @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::instance()->init()
*/
public static function init() {
wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' );
\Automattic\WooCommerce\RestApi\Server::instance()->init();
}
/**
* Return the version of the package.
*
* @deprecated since 4.5.0. This tracks WooCommerce version now.
* @return string
*/
public static function get_version() {
wc_deprecated_function( 'WC()->version', '4.5.0' );
return WC()->version;
}
/**
* Return the path to the package.
*
* @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path()
* @return string
*/
public static function get_path() {
wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' );
return \Automattic\WooCommerce\RestApi\Server::get_path();
}
}
Server.php 0000644 00000021227 15154275247 0006543 0 ustar 00 <?php
/**
* Initialize this version of the REST API.
*
* @package WooCommerce\RestApi
*/
namespace Automattic\WooCommerce\RestApi;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\RestApi\Utilities\SingletonTrait;
/**
* Class responsible for loading the REST API and all REST API namespaces.
*/
class Server {
use SingletonTrait;
/**
* REST API namespaces and endpoints.
*
* @var array
*/
protected $controllers = array();
/**
* Hook into WordPress ready to init the REST API as needed.
*/
public function init() {
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
\WC_REST_System_Status_V2_Controller::register_cache_clean();
}
/**
* Register REST API routes.
*/
public function register_rest_routes() {
foreach ( $this->get_rest_namespaces() as $namespace => $controllers ) {
foreach ( $controllers as $controller_name => $controller_class ) {
$this->controllers[ $namespace ][ $controller_name ] = new $controller_class();
$this->controllers[ $namespace ][ $controller_name ]->register_routes();
}
}
}
/**
* Get API namespaces - new namespaces should be registered here.
*
* @return array List of Namespaces and Main controller classes.
*/
protected function get_rest_namespaces() {
return apply_filters(
'woocommerce_rest_api_get_rest_namespaces',
array(
'wc/v1' => $this->get_v1_controllers(),
'wc/v2' => $this->get_v2_controllers(),
'wc/v3' => $this->get_v3_controllers(),
'wc-telemetry' => $this->get_telemetry_controllers(),
)
);
}
/**
* List of controllers in the wc/v1 namespace.
*
* @return array
*/
protected function get_v1_controllers() {
return array(
'coupons' => 'WC_REST_Coupons_V1_Controller',
'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller',
'customers' => 'WC_REST_Customers_V1_Controller',
'order-notes' => 'WC_REST_Order_Notes_V1_Controller',
'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller',
'orders' => 'WC_REST_Orders_V1_Controller',
'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller',
'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller',
'product-categories' => 'WC_REST_Product_Categories_V1_Controller',
'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller',
'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller',
'product-tags' => 'WC_REST_Product_Tags_V1_Controller',
'products' => 'WC_REST_Products_V1_Controller',
'reports-sales' => 'WC_REST_Report_Sales_V1_Controller',
'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller',
'reports' => 'WC_REST_Reports_V1_Controller',
'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller',
'taxes' => 'WC_REST_Taxes_V1_Controller',
'webhooks' => 'WC_REST_Webhooks_V1_Controller',
'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller',
);
}
/**
* List of controllers in the wc/v2 namespace.
*
* @return array
*/
protected function get_v2_controllers() {
return array(
'coupons' => 'WC_REST_Coupons_V2_Controller',
'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller',
'customers' => 'WC_REST_Customers_V2_Controller',
'network-orders' => 'WC_REST_Network_Orders_V2_Controller',
'order-notes' => 'WC_REST_Order_Notes_V2_Controller',
'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller',
'orders' => 'WC_REST_Orders_V2_Controller',
'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller',
'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller',
'product-categories' => 'WC_REST_Product_Categories_V2_Controller',
'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller',
'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller',
'product-tags' => 'WC_REST_Product_Tags_V2_Controller',
'products' => 'WC_REST_Products_V2_Controller',
'product-variations' => 'WC_REST_Product_Variations_V2_Controller',
'reports-sales' => 'WC_REST_Report_Sales_V2_Controller',
'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller',
'reports' => 'WC_REST_Reports_V2_Controller',
'settings' => 'WC_REST_Settings_V2_Controller',
'settings-options' => 'WC_REST_Setting_Options_V2_Controller',
'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller',
'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller',
'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller',
'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller',
'taxes' => 'WC_REST_Taxes_V2_Controller',
'webhooks' => 'WC_REST_Webhooks_V2_Controller',
'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller',
'system-status' => 'WC_REST_System_Status_V2_Controller',
'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller',
'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller',
'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller',
);
}
/**
* List of controllers in the wc/v3 namespace.
*
* @return array
*/
protected function get_v3_controllers() {
return array(
'coupons' => 'WC_REST_Coupons_Controller',
'customer-downloads' => 'WC_REST_Customer_Downloads_Controller',
'customers' => 'WC_REST_Customers_Controller',
'network-orders' => 'WC_REST_Network_Orders_Controller',
'order-notes' => 'WC_REST_Order_Notes_Controller',
'order-refunds' => 'WC_REST_Order_Refunds_Controller',
'orders' => 'WC_REST_Orders_Controller',
'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller',
'product-attributes' => 'WC_REST_Product_Attributes_Controller',
'product-categories' => 'WC_REST_Product_Categories_Controller',
'product-reviews' => 'WC_REST_Product_Reviews_Controller',
'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller',
'product-tags' => 'WC_REST_Product_Tags_Controller',
'products' => 'WC_REST_Products_Controller',
'product-variations' => 'WC_REST_Product_Variations_Controller',
'reports-sales' => 'WC_REST_Report_Sales_Controller',
'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller',
'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller',
'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller',
'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller',
'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller',
'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller',
'reports' => 'WC_REST_Reports_Controller',
'settings' => 'WC_REST_Settings_Controller',
'settings-options' => 'WC_REST_Setting_Options_Controller',
'shipping-zones' => 'WC_REST_Shipping_Zones_Controller',
'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller',
'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller',
'tax-classes' => 'WC_REST_Tax_Classes_Controller',
'taxes' => 'WC_REST_Taxes_Controller',
'webhooks' => 'WC_REST_Webhooks_Controller',
'system-status' => 'WC_REST_System_Status_Controller',
'system-status-tools' => 'WC_REST_System_Status_Tools_Controller',
'shipping-methods' => 'WC_REST_Shipping_Methods_Controller',
'payment-gateways' => 'WC_REST_Payment_Gateways_Controller',
'data' => 'WC_REST_Data_Controller',
'data-continents' => 'WC_REST_Data_Continents_Controller',
'data-countries' => 'WC_REST_Data_Countries_Controller',
'data-currencies' => 'WC_REST_Data_Currencies_Controller',
);
}
/**
* List of controllers in the telemetry namespace.
*
* @return array
*/
protected function get_telemetry_controllers() {
return array(
'tracker' => 'WC_REST_Telemetry_Controller',
);
}
/**
* Return the path to the package.
*
* @return string
*/
public static function get_path() {
return dirname( __DIR__ );
}
}
Utilities/ImageAttachment.php 0000644 00000003705 15154275247 0012304 0 ustar 00 <?php
/**
* Helper to upload files via the REST API.
*
* @package WooCommerce\Utilities
*/
namespace Automattic\WooCommerce\RestApi\Utilities;
/**
* ImageAttachment class.
*/
class ImageAttachment {
/**
* Attachment ID.
*
* @var integer
*/
public $id = 0;
/**
* Object attached to.
*
* @var integer
*/
public $object_id = 0;
/**
* Constructor.
*
* @param integer $id Attachment ID.
* @param integer $object_id Object ID.
*/
public function __construct( $id = 0, $object_id = 0 ) {
$this->id = (int) $id;
$this->object_id = (int) $object_id;
}
/**
* Upload an attachment file.
*
* @throws \WC_REST_Exception REST API exceptions.
* @param string $src URL to file.
*/
public function upload_image_from_src( $src ) {
$upload = wc_rest_upload_image_from_url( esc_url_raw( $src ) );
if ( is_wp_error( $upload ) ) {
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $this->object_id, $images ) ) {
throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 );
} else {
return;
}
}
$this->id = wc_rest_set_uploaded_image_as_attachment( $upload, $this->object_id );
if ( ! wp_attachment_is_image( $this->id ) ) {
/* translators: %s: image ID */
throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $this->id ), 400 );
}
}
/**
* Update attachment alt text.
*
* @param string $text Text to set.
*/
public function update_alt_text( $text ) {
if ( ! $this->id ) {
return;
}
update_post_meta( $this->id, '_wp_attachment_image_alt', wc_clean( $text ) );
}
/**
* Update attachment name.
*
* @param string $text Text to set.
*/
public function update_name( $text ) {
if ( ! $this->id ) {
return;
}
wp_update_post(
array(
'ID' => $this->id,
'post_title' => $text,
)
);
}
}
Utilities/SingletonTrait.php 0000644 00000001537 15154275247 0012220 0 ustar 00 <?php
/**
* Singleton class trait.
*
* @package WooCommerce\Utilities
*/
namespace Automattic\WooCommerce\RestApi\Utilities;
/**
* Singleton trait.
*/
trait SingletonTrait {
/**
* The single instance of the class.
*
* @var object
*/
protected static $instance = null;
/**
* Constructor
*
* @return void
*/
protected function __construct() {}
/**
* Get class instance.
*
* @return object Instance.
*/
final public static function instance() {
if ( null === static::$instance ) {
static::$instance = new static();
}
return static::$instance;
}
/**
* Prevent cloning.
*/
private function __clone() {}
/**
* Prevent unserializing.
*/
final public function __wakeup() {
wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '4.6' );
die();
}
}
class-wc-rest-wccom-site-installer-error-codes.php 0000644 00000011747 15155031160 0016240 0 ustar 00 <?php
/**
* WCCOM Site Installer Error Codes Class
*
* @package WooCommerce\WCCom\API
* @since 7.7.0
*/
defined( 'ABSPATH' ) || exit;
/**
* WCCOM Site Installer Error Codes Class
*
* Stores data for errors, returned by installer API.
*/
class WC_REST_WCCOM_Site_Installer_Error_Codes {
const NOT_AUTHENTICATED = 'not_authenticated';
const NO_ACCESS_TOKEN = 'no_access_token';
const NO_SIGNATURE = 'no_signature';
const SITE_NOT_CONNECTED = 'site_not_connnected';
const INVALID_TOKEN = 'invalid_token';
const REQUEST_VERIFICATION_FAILED = 'request_verification_failed';
const USER_NOT_FOUND = 'user_not_found';
const NO_PERMISSION = 'forbidden';
const IDEMPOTENCY_KEY_MISMATCH = 'idempotency_key_mismatch';
const NO_INITIATED_INSTALLATION_FOUND = 'no_initiated_installation_found';
const ALL_INSTALLATION_STEPS_RUN = 'all_installation_steps_run';
const REQUESTED_STEP_ALREADY_RUN = 'requested_step_already_run';
const PLUGIN_ALREADY_INSTALLED = 'plugin_already_installed';
const INSTALLATION_ALREADY_RUNNING = 'installation_already_running';
const INSTALLATION_FAILED = 'installation_failed';
const FILESYSTEM_REQUIREMENTS_NOT_MET = 'filesystem_requirements_not_met';
const FAILED_GETTING_PRODUCT_INFO = 'product_info_failed';
const INVALID_PRODUCT_INFO_RESPONSE = 'invalid_product_info_response';
const WCCOM_PRODUCT_MISSING_SUBSCRIPTION = 'wccom_product_missing_subscription';
const WCCOM_PRODUCT_MISSING_PACKAGE = 'wccom_product_missing_package';
const WPORG_PRODUCT_MISSING_DOWNLOAD_LINK = 'wporg_product_missing_download_link';
const MISSING_DOWNLOAD_PATH = 'missing_download_path';
const MISSING_UNPACKED_PATH = 'missing_unpacked_path';
const UNKNOWN_FILENAME = 'unknown_filename';
const PLUGIN_ACTIVATION_ERROR = 'plugin_activation_error';
const UNEXPECTED_ERROR = 'unexpected_error';
const FAILED_TO_RESET_INSTALLATION_STATE = 'failed_to_reset_installation_state';
const ERROR_MESSAGES = array(
self::NOT_AUTHENTICATED => 'Authentication required',
self::NO_ACCESS_TOKEN => 'No access token provided',
self::NO_SIGNATURE => 'No signature provided',
self::SITE_NOT_CONNECTED => 'Site not connected to WooCommerce.com',
self::INVALID_TOKEN => 'Invalid access token provided',
self::REQUEST_VERIFICATION_FAILED => 'Request verification by signature failed',
self::USER_NOT_FOUND => 'Token owning user not found',
self::NO_PERMISSION => 'You do not have permission to install plugin or theme',
self::IDEMPOTENCY_KEY_MISMATCH => 'Idempotency key mismatch',
self::NO_INITIATED_INSTALLATION_FOUND => 'No initiated installation for the product found',
self::ALL_INSTALLATION_STEPS_RUN => 'All installation steps have been run',
self::REQUESTED_STEP_ALREADY_RUN => 'Requested step has already been run',
self::PLUGIN_ALREADY_INSTALLED => 'The plugin has already been installed',
self::INSTALLATION_ALREADY_RUNNING => 'The installation of the plugin is already running',
self::INSTALLATION_FAILED => 'The installation of the plugin failed',
self::FILESYSTEM_REQUIREMENTS_NOT_MET => 'The filesystem requirements are not met',
self::FAILED_GETTING_PRODUCT_INFO => 'Failed to retrieve product info from woocommerce.com',
self::INVALID_PRODUCT_INFO_RESPONSE => 'Invalid product info response from woocommerce.com',
self::WCCOM_PRODUCT_MISSING_SUBSCRIPTION => 'Product subscription is missing',
self::WCCOM_PRODUCT_MISSING_PACKAGE => 'Could not find product package',
self::MISSING_DOWNLOAD_PATH => 'Download path is missing',
self::MISSING_UNPACKED_PATH => 'Unpacked path is missing',
self::UNKNOWN_FILENAME => 'Unknown product filename',
self::PLUGIN_ACTIVATION_ERROR => 'Plugin activation error',
self::UNEXPECTED_ERROR => 'Unexpected error',
self::FAILED_TO_RESET_INSTALLATION_STATE => 'Failed to reset installation state',
);
const HTTP_CODES = array(
self::NOT_AUTHENTICATED => 401,
self::NO_ACCESS_TOKEN => 400,
self::NO_SIGNATURE => 400,
self::SITE_NOT_CONNECTED => 401,
self::INVALID_TOKEN => 401,
self::REQUEST_VERIFICATION_FAILED => 400,
self::USER_NOT_FOUND => 401,
self::NO_PERMISSION => 403,
self::IDEMPOTENCY_KEY_MISMATCH => 400,
self::NO_INITIATED_INSTALLATION_FOUND => 400,
self::ALL_INSTALLATION_STEPS_RUN => 400,
self::REQUESTED_STEP_ALREADY_RUN => 400,
self::UNEXPECTED_ERROR => 500,
);
}
class-wc-rest-wccom-site-installer-error.php 0000644 00000002217 15155031160 0015135 0 ustar 00 <?php
/**
* WCCOM Site Installer Error Class
*
* @package WooCommerce\WCCom\API
* @since 7.7.0
*/
defined( 'ABSPATH' ) || exit;
/**
* WCCOM Site Installer Error Class
*/
class WC_REST_WCCOM_Site_Installer_Error extends Exception {
/**
* Constructor for the Installer Error class.
*
* @param string $error_code Error code.
* @param string $error_message Error message.
* @param int $http_code HTTP status code.
*/
public function __construct( $error_code, $error_message = null, $http_code = null ) {
$this->error_code = $error_code;
$this->error_message = $error_message ?? WC_REST_WCCOM_Site_Installer_Error_Codes::ERROR_MESSAGES[ $error_code ] ?? '';
$this->http_code = $http_code ?? WC_REST_WCCOM_Site_Installer_Error_Codes::HTTP_CODES[ $error_code ] ?? 400;
parent::__construct( $error_code );
}
/**
* Get the error code.
*/
public function get_error_code() {
return $this->error_code;
}
/**
* Get the error message.
*/
public function get_error_message() {
return $this->error_message;
}
/**
* Get the HTTP status code.
*/
public function get_http_code() {
return $this->http_code;
}
}
endpoints/class-wc-rest-wccom-site-installer-controller-v2.php 0000644 00000014577 15155031160 0020533 0 ustar 00 <?php
/**
* WCCOM Site Installer REST API Controller Version 2
*
* Handles requests to /installer.
*
* @package WooCommerce\WCCom\API
* @since 7.7.0
*/
use WC_REST_WCCOM_Site_Installer_Error_Codes as Installer_Error_Codes;
use WC_REST_WCCOM_Site_Installer_Error as Installer_Error;
defined( 'ABSPATH' ) || exit;
/**
* REST API WCCOM Site Installer Controller Class.
*
* @extends WC_REST_Controller
*/
class WC_REST_WCCOM_Site_Installer_Controller_V2 extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wccom-site/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'installer';
/**
* Register the routes for product reviews.
*
* @since 7.7.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( $this, 'install' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'product-id' => array(
'required' => true,
'type' => 'integer',
),
'run-until-step' => array(
'required' => true,
'type' => 'string',
'enum' => WC_WCCOM_Site_Installation_Manager::STEPS,
),
'idempotency-key' => array(
'required' => true,
'type' => 'string',
),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'reset_install' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'product-id' => array(
'required' => true,
'type' => 'integer',
),
'idempotency-key' => array(
'required' => true,
'type' => 'string',
),
),
),
)
);
}
/**
* Check permissions.
*
* @since 7.7.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function check_permission( $request ) {
$current_user = wp_get_current_user();
if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) {
/**
* This filter allows to provide a custom error message when the user is not authenticated.
*
* @since 3.7.0
*/
$error = apply_filters(
WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME,
new Installer_Error( Installer_Error_Codes::NOT_AUTHENTICATED )
);
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) {
$error = new Installer_Error( Installer_Error_Codes::NO_PERMISSION );
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
return true;
}
/**
* Install WooCommerce.com products.
*
* @since 7.7.0
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function install( $request ) {
try {
$product_id = $request['product-id'];
$run_until_step = $request['run-until-step'];
$idempotency_key = $request['idempotency-key'];
$installation_manager = new WC_WCCOM_Site_Installation_Manager( $product_id, $idempotency_key );
$installation_manager->run_installation( $run_until_step );
$response = $this->success_response( $product_id );
} catch ( Installer_Error $exception ) {
$response = $this->failure_response( $product_id, $exception );
}
return $response;
}
/**
* Reset installation state.
*
* @since 7.7.0
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function reset_install( $request ) {
try {
$product_id = $request['product-id'];
$idempotency_key = $request['idempotency-key'];
$installation_manager = new WC_WCCOM_Site_Installation_Manager( $product_id, $idempotency_key );
$installation_manager->reset_installation();
$response = $this->success_response( $product_id );
} catch ( Installer_Error $exception ) {
$response = $this->failure_response( $product_id, $exception );
}
return $response;
}
/**
* Generate a standardized response for a successful request.
*
* @param int $product_id Product ID.
* @return WP_REST_Response|WP_Error
*/
protected function success_response( $product_id ) {
$state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id );
$response = rest_ensure_response(
array(
'success' => true,
'state' => $state ? $this->map_state_to_response( $state ) : null,
)
);
$response->set_status( 200 );
return $response;
}
/**
* Generate a standardized response for a failed request.
*
* @param int $product_id Product ID.
* @param Installer_Error $exception The exception.
* @return WP_REST_Response|WP_Error
*/
protected function failure_response( $product_id, $exception ) {
$state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id );
$response = rest_ensure_response(
array(
'success' => false,
'error_code' => $exception->get_error_code(),
'error_message' => $exception->get_error_message(),
'state' => $state ? $this->map_state_to_response( $state ) : null,
)
);
$response->set_status( $exception->get_http_code() );
return $response;
}
/**
* Map the installation state to a response.
*
* @param WC_WCCOM_Site_Installation_State $state The installation state.
* @return array
*/
protected function map_state_to_response( $state ) {
return array(
'product_id' => $state->get_product_id(),
'idempotency_key' => $state->get_idempotency_key(),
'last_step_name' => $state->get_last_step_name(),
'last_step_status' => $state->get_last_step_status(),
'last_step_error' => $state->get_last_step_error(),
'product_type' => $state->get_product_type(),
'product_name' => $state->get_product_name(),
'already_installed_plugin_info' => $state->get_already_installed_plugin_info(),
'started_seconds_ago' => time() - $state->get_started_date(),
);
}
}
endpoints/class-wc-rest-wccom-site-installer-controller.php 0000644 00000011606 15155031160 0020174 0 ustar 00 <?php
/**
* WCCOM Site Installer REST API Controller
*
* Handles requests to /installer.
*
* @package WooCommerce\WCCom\API
* @since 3.7.0
*/
use WC_REST_WCCOM_Site_Installer_Error_Codes as Installer_Error_Codes;
use WC_REST_WCCOM_Site_Installer_Error as Installer_Error;
defined( 'ABSPATH' ) || exit;
/**
* REST API WCCOM Site Installer Controller Class.
*
* @extends WC_REST_Controller
*/
class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wccom-site/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'installer';
/**
* Register the routes for WCCCOM Installer Controller.
*
* @since 3.7.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_install_state' ),
'permission_callback' => array( $this, 'check_permission' ),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'install' ),
'permission_callback' => array( $this, 'check_permission' ),
'args' => array(
'products' => array(
'required' => true,
'type' => 'object',
),
),
),
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'reset_install' ),
'permission_callback' => array( $this, 'check_permission' ),
),
)
);
}
/**
* Check permissions.
*
* @since 3.7.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function check_permission( $request ) {
$current_user = wp_get_current_user();
if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) {
/**
* This filter allows to provide a custom error message when the user is not authenticated.
*
* @since 3.7.0
*/
$error = apply_filters(
WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME,
new Installer_Error( Installer_Error_Codes::NOT_AUTHENTICATED )
);
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) {
$error = new Installer_Error( Installer_Error_Codes::NO_PERMISSION );
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
return true;
}
/**
* Get installation state.
*
* @since 3.7.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function get_install_state( $request ) {
$requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements();
if ( is_wp_error( $requirements_met ) ) {
return $requirements_met;
}
return rest_ensure_response( WC_WCCOM_Site_Installer::get_state() );
}
/**
* Install WooCommerce.com products.
*
* @since 3.7.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function install( $request ) {
$requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements();
if ( is_wp_error( $requirements_met ) ) {
return $requirements_met;
}
if ( empty( $request['products'] ) ) {
return new WP_Error( 'missing_products', __( 'Missing products in request body.', 'woocommerce' ), array( 'status' => 400 ) );
}
$validation_result = $this->validate_products( $request['products'] );
if ( is_wp_error( $validation_result ) ) {
return $validation_result;
}
return rest_ensure_response( WC_WCCOM_Site_Installer::schedule_install( $request['products'] ) );
}
/**
* Reset installation state.
*
* @since 3.7.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function reset_install( $request ) {
$resp = rest_ensure_response( WC_WCCOM_Site_Installer::reset_state() );
$resp->set_status( 204 );
return $resp;
}
/**
* Validate products from request body.
*
* @since 3.7.0
* @param array $products Array of products where key is product ID and
* element is install args.
* @return bool|WP_Error
*/
protected function validate_products( $products ) {
$err = new WP_Error( 'invalid_products', __( 'Invalid products in request body.', 'woocommerce' ), array( 'status' => 400 ) );
if ( ! is_array( $products ) ) {
return $err;
}
foreach ( $products as $product_id => $install_args ) {
if ( ! absint( $product_id ) ) {
return $err;
}
if ( empty( $install_args ) || ! is_array( $install_args ) ) {
return $err;
}
}
return true;
}
}
endpoints/class-wc-rest-wccom-site-ssr-controller.php 0000644 00000006451 15155031160 0017010 0 ustar 00 <?php
/**
* WCCOM Site System Status Report REST API Controller
*
* Handles requests to /ssr.
*
* @package WooCommerce\WCCom\API
* @since 7.8.0
*/
use WC_REST_WCCOM_Site_Installer_Error_Codes as Installer_Error_Codes;
use WC_REST_WCCOM_Site_Installer_Error as Installer_Error;
defined( 'ABSPATH' ) || exit;
/**
* REST API WCCOM System Status Report Controller Class.
*
* @extends WC_REST_Controller
*/
class WC_REST_WCCOM_Site_SSR_Controller extends WC_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wccom-site/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'ssr';
/**
* Register the routes for SSR Controller.
*
* @since 7.8.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'handle_ssr_request' ),
'permission_callback' => array( $this, 'check_permission' ),
),
),
);
}
/**
* Check permissions.
*
* Please note that access to this endpoint is also governed by the WC_WCCOM_Site::authenticate_wccom() method.
*
* @since 7.8.0
* @param WP_REST_Request $request Full details about the request.
* @return bool|WP_Error
*/
public function check_permission( $request ) {
$current_user = wp_get_current_user();
if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) {
/**
* This filter allows to provide a custom error message when the user is not authenticated.
*
* @since 7.8.0
*/
$error = apply_filters(
WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME,
new Installer_Error( Installer_Error_Codes::NOT_AUTHENTICATED )
);
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
if ( ! user_can( $current_user, 'manage_woocommerce' ) ) {
$error = new Installer_Error( Installer_Error_Codes::NO_PERMISSION );
return new WP_Error(
$error->get_error_code(),
$error->get_error_message(),
array( 'status' => $error->get_http_code() )
);
}
return true;
}
/**
* Generate SSR data and submit it to WooCommmerce.com.
*
* @since 7.8.0
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response
*/
public function handle_ssr_request( $request ) {
$ssr_controller = new WC_REST_System_Status_Controller();
$data = $ssr_controller->get_items( $request );
$data = $data->get_data();
// Submit SSR data to WooCommerce.com.
$request = WC_Helper_API::post(
'ssr',
array(
'body' => wp_json_encode( array( 'data' => $data ) ),
'authenticated' => true,
)
);
$response_code = wp_remote_retrieve_response_code( $request );
if ( 201 === $response_code ) {
$response = rest_ensure_response(
array(
'success' => true,
'message' => 'SSR data submitted successfully',
)
);
} else {
$response = rest_ensure_response(
array(
'success' => false,
'error_code' => 'failed_submitting_ssr',
'error_message' => "Submitting SSR data failed with response code: $response_code",
)
);
}
return $response;
}
}