2019-05-10 16:56:07 +00: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
2020-09-17 14:56:08 +00:00
* @ package WooCommerce\RestApi
2019-05-10 16:56:07 +00:00
* @ see https :// developer . wordpress . org / rest - api / extending - the - rest - api / controller - classes /
*/
if ( ! defined ( 'ABSPATH' ) ) {
exit ;
}
/**
* Abstract Rest Controller Class
*
2020-09-17 14:56:08 +00:00
* @ package WooCommerce\RestApi
2019-05-10 16:56:07 +00:00
* @ 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 = '' ;
2020-06-03 12:19:30 +00:00
/**
* 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 ;
2019-05-10 16:56:07 +00:00
/**
* 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 ;
}
2020-08-05 16:14:27 +00:00
/**
* 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 ;
}
2020-08-13 08:00:39 +00:00
$endpoint_args = $this -> adjust_wp_5_5_datatype_compatibility ( $endpoint_args );
return $endpoint_args ;
}
/**
2020-08-13 11:06:27 +00:00
* 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 .
2020-08-13 08:00:39 +00:00
*
* @ 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 ;
}
2020-08-05 16:14:27 +00:00
foreach ( $endpoint_args as $field_id => $params ) {
2020-08-13 08:00:39 +00:00
if ( ! isset ( $params [ 'type' ] ) ) {
continue ;
}
2020-08-05 16:14:27 +00:00
/**
2020-08-13 08:00:39 +00:00
* Custom types are not supported as of WP 5.5 , this translates type => 'date-time' to type => 'string' .
2020-08-05 16:14:27 +00:00
*/
if ( 'date-time' === $params [ 'type' ] ) {
2020-08-13 11:19:44 +00:00
$params [ 'type' ] = array ( 'null' , 'string' );
2020-08-13 08:00:39 +00:00
}
/**
* 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' ] ) {
2020-08-13 08:47:48 +00:00
$params [ 'type' ] = array ( 'null' , 'object' , 'string' , 'number' , 'boolean' , 'integer' , 'array' );
2020-08-13 08:00:39 +00:00
}
if ( isset ( $params [ 'properties' ] ) ) {
$params [ 'properties' ] = $this -> adjust_wp_5_5_datatype_compatibility ( $params [ 'properties' ] );
2020-08-05 16:14:27 +00:00
}
2020-08-13 08:00:39 +00:00
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 ;
}
2020-08-05 16:14:27 +00:00
return $endpoint_args ;
}
2019-05-10 16:56:07 +00:00
/**
* 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 */
2020-08-06 12:48:18 +00:00
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 ) );
2019-05-10 16:56:07 +00:00
}
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 () );
2019-06-26 14:26:58 +00:00
$query = $request -> get_query_params ();
2019-05-10 16:56:07 +00:00
$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' );
// 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 );
2019-06-26 14:26:58 +00:00
// Set query (GET) parameters.
$_item -> set_query_params ( $query );
2019-05-10 16:56:07 +00:00
$_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' );
$_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' );
$_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 {
2020-08-06 12:48:18 +00:00
return new WP_Error ( 'rest_setting_value_invalid' , __ ( 'An invalid setting value was passed.' , 'woocommerce' ), array ( 'status' => 400 ) );
2019-05-10 16:56:07 +00:00
}
}
/**
* 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 ) ) {
2020-08-06 12:48:18 +00:00
return new WP_Error ( 'rest_setting_value_invalid' , __ ( 'An invalid setting value was passed.' , 'woocommerce' ), array ( 'status' => 400 ) );
2019-05-10 16:56:07 +00:00
}
$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 ) ) {
2020-08-06 12:48:18 +00:00
return new WP_Error ( 'rest_setting_value_invalid' , __ ( 'An invalid setting value was passed.' , 'woocommerce' ), array ( 'status' => 400 ) );
2019-05-10 16:56:07 +00:00
}
$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 {
2020-08-06 12:48:18 +00:00
return new WP_Error ( 'rest_setting_value_invalid' , __ ( 'An invalid setting value was passed.' , 'woocommerce' ), array ( 'status' => 400 ) );
2019-05-10 16:56:07 +00:00
}
}
/**
* 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 (
2020-08-06 12:48:18 +00:00
'description' => __ ( 'List of created resources.' , 'woocommerce' ),
2019-05-10 16:56:07 +00:00
'type' => 'array' ,
'context' => array ( 'view' , 'edit' ),
'items' => array (
'type' => 'object' ,
),
),
'update' => array (
2020-08-06 12:48:18 +00:00
'description' => __ ( 'List of updated resources.' , 'woocommerce' ),
2019-05-10 16:56:07 +00:00
'type' => 'array' ,
'context' => array ( 'view' , 'edit' ),
'items' => array (
'type' => 'object' ,
),
),
'delete' => array (
2020-08-06 12:48:18 +00:00
'description' => __ ( 'List of delete resources.' , 'woocommerce' ),
2019-05-10 16:56:07 +00:00
'type' => 'array' ,
'context' => array ( 'view' , 'edit' ),
'items' => array (
'type' => 'integer' ,
),
),
),
);
return $schema ;
}
/**
* Gets an array of fields to be included on the response .
2020-01-07 18:50:35 +00:00
*
2019-05-10 16:56:07 +00:00
* Included fields are based on item schema and `_fields=` request argument .
2020-01-07 18:50:35 +00:00
* Updated from WordPress 5.3 , included into this class to support old versions .
2019-05-10 16:56:07 +00:00
*
* @ 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 ) {
2020-06-03 12:19:30 +00:00
// 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 ;
}
2020-06-03 13:14:05 +00:00
$this -> _request = $request ;
2020-06-03 12:19:30 +00:00
2020-01-07 18:50:35 +00:00
$schema = $this -> get_item_schema ();
$properties = isset ( $schema [ 'properties' ] ) ? $schema [ 'properties' ] : array ();
2019-05-10 16:56:07 +00:00
$additional_fields = $this -> get_additional_fields ();
2020-06-02 17:20:00 +00:00
2019-05-10 16:56:07 +00:00
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' ] ) ) {
2020-01-07 18:50:35 +00:00
$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 ] );
}
2019-05-10 16:56:07 +00:00
}
}
2020-01-07 18:50:35 +00:00
$fields = array_keys ( $properties );
2019-05-10 16:56:07 +00:00
if ( ! isset ( $request [ '_fields' ] ) ) {
2020-06-03 13:14:05 +00:00
$this -> _fields = $fields ;
2019-05-10 16:56:07 +00:00
return $fields ;
}
2020-01-07 18:50:35 +00:00
$requested_fields = wp_parse_list ( $request [ '_fields' ] );
2019-05-10 16:56:07 +00:00
if ( 0 === count ( $requested_fields ) ) {
2020-06-03 13:14:05 +00:00
$this -> _fields = $fields ;
2019-05-10 16:56:07 +00:00
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' ;
}
2020-01-07 18:50:35 +00:00
// Return the list of all requested fields which appear in the schema.
2020-06-03 12:19:30 +00:00
$this -> _fields = array_reduce (
2020-01-07 18:50:35 +00:00
$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 );
2020-06-02 17:20:00 +00:00
// A nested field is included so long as its top-level property
// is present in the schema.
2020-01-07 18:50:35 +00:00
if ( in_array ( $nested_fields [ 0 ], $fields , true ) ) {
$response_fields [] = $field ;
}
return $response_fields ;
},
array ()
);
2020-06-03 12:19:30 +00:00
return $this -> _fields ;
2019-05-10 16:56:07 +00:00
}
}