diff --git a/includes/api/v1/class-wc-rest-coupons-controller.php b/includes/api/v1/class-wc-rest-coupons-controller.php new file mode 100644 index 00000000000..b1c7c4d65a4 --- /dev/null +++ b/includes/api/v1/class-wc-rest-coupons-controller.php @@ -0,0 +1,609 @@ +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[\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 + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + if ( ! empty( $request['code'] ) ) { + $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $request['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 ) { + global $wpdb; + + // Get the coupon code. + $code = $wpdb->get_var( $wpdb->prepare( "SELECT post_title FROM $wpdb->posts WHERE id = %s AND post_type = 'shop_coupon' AND post_status = 'publish'", $post->ID ) ); + + $coupon = new WC_Coupon( $code ); + + $data = array( + 'id' => $coupon->id, + 'code' => $coupon->code, + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'discount_type' => $coupon->type, + 'description' => $post->post_excerpt, + 'amount' => wc_format_decimal( $coupon->coupon_amount, 2 ), + 'expiry_date' => $coupon->expiry_date ? wc_rest_prepare_date_response( date( 'Y-m-d', $coupon->expiry_date ) ) : null, + 'usage_count' => (int) $coupon->usage_count, + 'individual_use' => ( 'yes' === $coupon->individual_use ), + 'product_ids' => array_map( 'absint', (array) $coupon->product_ids ), + 'exclude_product_ids' => array_map( 'absint', (array) $coupon->exclude_product_ids ), + 'usage_limit' => ( ! empty( $coupon->usage_limit ) ) ? $coupon->usage_limit : null, + 'usage_limit_per_user' => ( ! empty( $coupon->usage_limit_per_user ) ) ? $coupon->usage_limit_per_user : null, + 'limit_usage_to_x_items' => (int) $coupon->limit_usage_to_x_items, + 'free_shipping' => $coupon->enable_free_shipping(), + 'product_categories' => array_map( 'absint', (array) $coupon->product_categories ), + 'excluded_product_categories' => array_map( 'absint', (array) $coupon->exclude_product_categories ), + 'exclude_sale_items' => $coupon->exclude_sale_items(), + 'minimum_amount' => wc_format_decimal( $coupon->minimum_amount, 2 ), + 'maximum_amount' => wc_format_decimal( $coupon->maximum_amount, 2 ), + 'email_restrictions' => $coupon->customer_email, + 'used_by' => $coupon->get_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 ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $post ) ); + + /** + * 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 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 ) { + global $wpdb; + + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Code. + if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { + $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); + $id = isset( $data->ID ) ? $data->ID : 0; + + // Check for duplicate coupon codes. + $coupon_found = $wpdb->get_var( $wpdb->prepare( " + SELECT $wpdb->posts.ID + FROM $wpdb->posts + WHERE $wpdb->posts.post_type = 'shop_coupon' + AND $wpdb->posts.post_status = 'publish' + AND $wpdb->posts.post_title = '%s' + AND $wpdb->posts.ID != %s + ", $coupon_code, $id ) ); + + if ( $coupon_found ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data->post_title = $coupon_code; + } + + // Content. + $data->post_content = ''; + + // Coupon description (excerpt). + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['description'] ); + } + + // Post type. + $data->post_type = $this->post_type; + + // Post status. + $data->post_status = 'publish'; + + // 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 ); + } + + /** + * Expiry date format. + * + * @param string $expiry_date + * @return string + */ + protected function get_coupon_expiry_date( $expiry_date ) { + if ( '' !== $expiry_date ) { + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + return ''; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + $data = array_filter( $request->get_params() ); + + $defaults = array( + 'discount_type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'email_restrictions' => array(), + 'description' => '' + ); + + $data = wp_parse_args( $data, $defaults ); + + // Set coupon meta. + update_post_meta( $post->ID, 'discount_type', $data['discount_type'] ); + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'usage_limit', absint( $data['usage_limit'] ) ); + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $post->ID, 'free_shipping', ( true === $data['free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_categories'] ) ) ); + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['excluded_product_categories'] ) ) ); + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['email_restrictions'] ) ) ); + + return true; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + if ( isset( $request['amount'] ) ) { + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $request['amount'] ) ); + } + + if ( isset( $request['individual_use'] ) ) { + update_post_meta( $post->ID, 'individual_use', ( true === $request['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_ids'] ) ) { + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $request['product_ids'] ) ) ) ); + } + + if ( isset( $request['exclude_product_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $request['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $request['usage_limit'] ) ) { + update_post_meta( $post->ID, 'usage_limit', absint( $request['usage_limit'] ) ); + } + + if ( isset( $request['usage_limit_per_user'] ) ) { + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $request['usage_limit_per_user'] ) ); + } + + if ( isset( $request['limit_usage_to_x_items'] ) ) { + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $request['limit_usage_to_x_items'] ) ); + } + + if ( isset( $request['usage_count'] ) ) { + update_post_meta( $post->ID, 'usage_count', absint( $request['usage_count'] ) ); + } + + if ( isset( $request['expiry_date'] ) ) { + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); + } + + if ( isset( $request['free_shipping'] ) ) { + update_post_meta( $post->ID, 'free_shipping', ( true === $request['free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_categories'] ) ) { + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_categories'] ) ) ); + } + + if ( isset( $request['excluded_product_categories'] ) ) { + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['excluded_product_categories'] ) ) ); + } + + if ( isset( $request['exclude_sale_items'] ) ) { + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $request['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['minimum_amount'] ) ) { + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $request['minimum_amount'] ) ); + } + + if ( isset( $request['maximum_amount'] ) ) { + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); + } + + if ( isset( $request['email_restrictions'] ) ) { + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['email_restrictions'] ) ) ); + } + + return true; + } + + /** + * 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.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + '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' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product ID's 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 ID's 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.', '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' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'Define if should not apply when have sale items.', '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 who 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; + } +} diff --git a/includes/api/v1/class-wc-rest-customer-downloads-controller.php b/includes/api/v1/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..575291748fa --- /dev/null +++ b/includes/api/v1/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,249 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce/API + * @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[\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 doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_user_permissions( 'read', $customer->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']; + + $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[\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' => __( 'Amount of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when the 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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-customers-controller.php b/includes/api/v1/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..75a918531ee --- /dev/null +++ b/includes/api/v1/class-wc-rest-customers-controller.php @@ -0,0 +1,916 @@ +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[\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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 manipule 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 ( '' !== $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 pagation 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 ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer_id = wc_create_new_customer( $request['email'], $request['username'], $request['password'] ); + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + $customer = get_user_by( 'id', $customer_id ); + + $this->update_additional_fields_for_object( $customer, $request ); + + // Add customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * 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', $customer, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $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_id ) ) ); + + return $response; + } + + /** + * 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']; + $customer = get_userdata( $id ); + + if ( empty( $id ) || empty( $customer->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $customer, $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 ) { + $id = (int) $request['id']; + $customer = get_userdata( $id ); + + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->user_email ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->user_login ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_email' => sanitize_email( $request['email'] ) ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + wp_update_user( array( 'ID' => $customer->ID, 'user_pass' => wc_clean( $request['password'] ) ) ); + } + + $this->update_additional_fields_for_object( $customer, $request ); + + // Update customer data. + $this->update_customer_meta_fields( $customer, $request ); + + /** + * 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', $customer, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $customer, $request ); + $response = rest_ensure_response( $response ); + return $response; + } + + /** + * 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 ) ); + } + + $customer = get_userdata( $id ); + if ( ! $customer ) { + 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( $customer, $request ); + + /** Include admin customer 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( '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 $customer The customer 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', $customer, $response, $request ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $customer Customer object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $customer, $request ) { + $last_order = wc_get_customer_last_order( $customer->ID ); + + $data = array( + 'id' => $customer->ID, + 'date_created' => wc_rest_prepare_date_response( $customer->user_registered ), + 'date_modified' => $customer->last_update ? wc_rest_prepare_date_response( date( 'Y-m-d H:i:s', $customer->last_update ) ) : null, + 'email' => $customer->user_email, + 'first_name' => $customer->first_name, + 'last_name' => $customer->last_name, + 'username' => $customer->user_login, + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->id : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->post->post_date_gmt ) : null + ), + 'orders_count' => wc_get_customer_order_count( $customer->ID ), + 'total_spent' => wc_format_decimal( wc_get_customer_total_spent( $customer->ID ), 2 ), + 'avatar_url' => wc_get_customer_avatar_url( $customer->customer_email ), + 'billing' => array( + 'first_name' => $customer->billing_first_name, + 'last_name' => $customer->billing_last_name, + 'company' => $customer->billing_company, + 'address_1' => $customer->billing_address_1, + 'address_2' => $customer->billing_address_2, + 'city' => $customer->billing_city, + 'state' => $customer->billing_state, + 'postcode' => $customer->billing_postcode, + 'country' => $customer->billing_country, + 'email' => $customer->billing_email, + 'phone' => $customer->billing_phone, + ), + 'shipping' => array( + 'first_name' => $customer->shipping_first_name, + 'last_name' => $customer->shipping_last_name, + 'company' => $customer->shipping_company, + 'address_1' => $customer->shipping_address_1, + 'address_2' => $customer->shipping_address_2, + 'city' => $customer->shipping_city, + 'state' => $customer->shipping_state, + 'postcode' => $customer->shipping_postcode, + 'country' => $customer->shipping_country, + ), + ); + + $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( $customer ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $customer User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $customer, $request ); + } + + /** + * Update customer meta fields. + * + * @param WP_User $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'] ) ) { + update_user_meta( $customer->ID, 'first_name', wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + update_user_meta( $customer->ID, 'last_name', wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing'] ) ) { + foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $address ) { + if ( isset( $request['billing'][ $address ] ) ) { + update_user_meta( $customer->ID, 'billing_' . $address, wc_clean( $request['billing'][ $address ] ) ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping'] ) ) { + foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $address ) { + if ( isset( $request['shipping'][ $address ] ) ) { + update_user_meta( $customer->ID, 'shipping_' . $address, wc_clean( $request['shipping'][ $address ] ) ); + } + } + } + } + + /** + * 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, in the site's timezone.", '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, + ), + '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' => __( 'UTC DateTime of the customer last order.', '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; + } +} diff --git a/includes/api/v1/class-wc-rest-order-notes-controller.php b/includes/api/v1/class-wc-rest-order-notes-controller.php new file mode 100644 index 00000000000..38dbd26a61d --- /dev/null +++ b/includes/api/v1/class-wc-rest-order-notes-controller.php @@ -0,0 +1,437 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce/API + * @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[\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[\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 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 a order note. + * + * @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['order_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 delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['order_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; + } + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->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'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order = wc_get_order( $order ); + + // 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[\d]+)', $order->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 = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || $this->post_type !== $order->post_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->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 = get_post( (int) $request['order_id'] ); + + if ( empty( $order->post_type ) || $this->post_type !== $order->post_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->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 = wp_delete_comment( $note->comment_ID, true ); + + 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[\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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-order-refunds-controller.php b/includes/api/v1/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..8f80553e80f --- /dev/null +++ b/includes/api/v1/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,541 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce/API + * @extends WC_REST_Posts_Controller + */ +class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Posts_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\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[\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_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + global $wpdb; + + $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 || intval( $refund->post->post_parent ) !== intval( $order->id ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = $request['dp']; + + $data = array( + 'id' => $refund->id, + 'date_created' => wc_rest_prepare_date_response( $refund->date ), + 'amount' => wc_format_decimal( $refund->get_refund_amount(), $dp ), + 'reason' => $refund->get_refund_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $refund->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['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. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund, $request ) { + $order_id = $refund->post->post_parent; + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->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 + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = 'any'; + + 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'] ) ) { + 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 ); + } + + $api_refund = is_bool( $request['api_refund'] ) ? $request['api_refund'] : true; + + $data = array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'line_items' => $request['line_items'], + ); + + // Create the refund. + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API. + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_data ); + + if ( isset( $payment_gateways[ $order->payment_method ] ) && $payment_gateways[ $order->payment_method ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->payment_method ]->process_refund( $order->id, $refund->get_refund_amount(), $refund->get_refund_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + return new WP_Error( 'woocommerce_rest_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + $post = get_post( $refund->id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_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' ), + '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' => 'string', + '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' => 'integer', + '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' => '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['dp'] = array( + 'default' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-orders-controller.php b/includes/api/v1/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..8eeb8149d1c --- /dev/null +++ b/includes/api/v1/class-wc-rest-orders-controller.php @@ -0,0 +1,1851 @@ +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[\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 ) { + global $wpdb; + + $order = wc_get_order( $post ); + $dp = $request['dp']; + + $data = array( + 'id' => $order->id, + 'parent_id' => $post->post_parent, + 'status' => $order->get_status(), + 'order_key' => $order->order_key, + 'number' => $order->get_order_number(), + 'currency' => $order->get_order_currency(), + 'version' => $order->order_version, + 'prices_include_tax' => $order->prices_include_tax, + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'customer_id' => $order->get_user_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->cart_discount_tax, $dp ), + 'shipping_total' => wc_format_decimal( $order->get_total_shipping(), $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->payment_method, + 'payment_method_title' => $order->payment_method_title, + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->customer_ip_address, + 'customer_user_agent' => $order->customer_user_agent, + 'created_via' => $order->created_via, + 'customer_note' => $order->customer_note, + 'date_completed' => wc_rest_prepare_date_response( $order->completed_date ), + 'date_paid' => $order->paid_date, + 'cart_hash' => $order->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 = $order->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $product->id; + $variation_id = $product->variation_id; + $product_sku = $product->get_sku(); + } + + $meta = new WC_Order_Item_Meta( $item, $product ); + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $meta->get_formatted( $hideprefix ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta['key'], + 'label' => $formatted_meta['label'], + 'value' => $formatted_meta['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 = maybe_unserialize( $shipping_item['taxes'] ); + + if ( ! empty( $shipping_taxes ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes ), $dp ); + + foreach ( $shipping_taxes 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->id, + 'refund' => $refund->get_refund_reason() ? $refund->get_refund_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_refund_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. + * @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->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->post->post_parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->post->post_parent ) ), + ); + } + + 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 ( ! empty( $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|stdClass $data Object. + */ + protected function prepare_item_for_database( $request ) { + $data = new stdClass; + + // Set default order args. + $data->status = $request['status']; + $data->customer_id = $request['customer_id']; + $data->customer_note = $request['customer_note']; + + /** + * 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 the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Create base WC Order object. + * + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + wc_transaction_query( 'start' ); + + try { + // Make sure customer exists. + if ( 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 ); + } + + $data = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $data ) ) { + return $data; + } + + $data->created_via = 'rest-api'; + + $order = $this->create_base_order( (array) $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order', sprintf( __( 'Cannot create order: %s.', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // Set addresses. + if ( is_array( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( is_array( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + // Set currency. + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + + // Set lines. + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( is_array( $request[ $line ] ) ) { + foreach ( $request[ $line ] as $item ) { + $set_item = 'set_' . $line_type; + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // Calculate totals and set them. + $order->calculate_totals(); + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method_title'] ); + } + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // Set meta data. + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); + } + + wc_transaction_query( 'commit' ); + + return $order->id; + } catch ( WC_REST_Exception $e ) { + wc_transaction_query( 'rollback' ); + + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + $fields = $order->get_address( $type ); + + foreach ( array_keys( $fields ) as $field ) { + if ( isset( $posted[ $field ] ) ) { + $fields[ $field ] = $posted[ $field ]; + } + } + + // Set address. + $order->set_address( $fields, $type ); + + // Update user meta. + if ( $order->get_user_id() ) { + foreach ( $fields as $key => $value ) { + update_user_meta( $order->get_user_id(), $type . '_' . $key, $value ); + } + } + } + + /** + * Create or update a line item. + * + * @param WC_Order $order Order data. + * @param array $item Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_line_item( $order, $item, $action = 'create' ) { + $creating = 'create' === $action; + $item_args = array(); + + // Product is always required. + if ( empty( $item['product_id'] ) && empty( $item['sku'] ) && empty( $item['variation_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $item['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $item['sku'] ); + } elseif ( ! empty( $item['product_id'] ) && empty( $item['variation_id'] ) ) { + $product_id = (int) $item['product_id']; + } elseif ( ! empty( $item['variation_id'] ) ) { + $product_id = (int) $item['variation_id']; + } + + // When updating, ensure product ID provided matches. + if ( 'update' === $action && ! empty( $item['id'] ) ) { + $item_product_id = (int) wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = (int) wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $product_id !== $item_product_id && $product_id !== $item_variation_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or variation ID provided does not match this line item.', 'woocommerce' ), 400 ); + } + } + + $product = wc_get_product( $product_id ); + + // Must be a valid WC_Product. + if ( ! is_object( $product ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // Quantity must be positive float. + if ( isset( $item['quantity'] ) && 0 >= floatval( $item['quantity'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // Quantity is required when creating. + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // Get variation attributes. + if ( method_exists( $product, 'get_variation_attributes' ) ) { + $item_args['variation'] = $product->get_variation_attributes(); + } + + // Quantity. + if ( isset( $item['quantity'] ) ) { + $item_args['qty'] = $item['quantity']; + } + + // Total. + if ( isset( $item['total'] ) ) { + $item_args['totals']['total'] = floatval( $item['total'] ); + } + + // Total tax. + if ( isset( $item['total_tax'] ) ) { + $item_args['totals']['tax'] = floatval( $item['total_tax'] ); + } + + // Subtotal. + if ( isset( $item['subtotal'] ) ) { + $item_args['totals']['subtotal'] = floatval( $item['subtotal'] ); + } + + // Subtotal tax. + if ( isset( $item['subtotal_tax'] ) ) { + $item_args['totals']['subtotal_tax'] = floatval( $item['subtotal_tax'] ); + } + + if ( $creating ) { + $item_id = $order->add_product( $product, $item_args['qty'], $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } else { + $item_id = $order->update_product( $item['id'], $product, $item_args ); + if ( ! $item_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_line_item', __( 'Cannot update line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order shipping method. + * + * @param WC_Order $order Order data. + * @param array $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_shipping( $order, $shipping, $action ) { + // Total must be a positive float. + if ( ! empty( $shipping['total'] ) && 0 > floatval( $shipping['total'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Method ID is required. + if ( empty( $shipping['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + + $shipping_id = $order->add_shipping( $rate ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_shipping', __( 'Cannot create shipping method, try again.', 'woocommerce' ), 500 ); + } + + } else { + $shipping_args = array(); + + if ( isset( $shipping['method_id'] ) ) { + $shipping_args['method_id'] = $shipping['method_id']; + } + + if ( isset( $shipping['method_title'] ) ) { + $shipping_args['method_title'] = $shipping['method_title']; + } + + if ( isset( $shipping['total'] ) ) { + $shipping_args['cost'] = floatval( $shipping['total'] ); + } + + $shipping_id = $order->update_shipping( $shipping['id'], $shipping_args ); + + if ( ! $shipping_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee. + * + * @param WC_Order $order Order data. + * @param array $fee Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_fee( $order, $fee, $action ) { + if ( 'create' === $action ) { + + // Fee name is required. + if ( empty( $fee['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + + $fee_data = new stdClass(); + $fee_data->id = sanitize_title( $fee['name'] ); + $fee_data->name = $fee['name']; + $fee_data->amount = isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0; + $fee_data->taxable = false; + $fee_data->tax = 0; + $fee_data->tax_data = array(); + $fee_data->tax_class = ''; + + // If taxable, tax class and total are required. + if ( isset( $fee['tax_status'] ) && 'taxable' === $fee['tax_status'] ) { + + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $fee_data->taxable = true; + $fee_data->tax_class = $fee['tax_class']; + + if ( isset( $fee['total_tax'] ) ) { + $fee_data->tax = isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0; + } + } + + $fee_id = $order->add_fee( $fee_data ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_fee', __( 'Cannot create fee, try again.', 'woocommerce' ), 500 ); + } + + } else { + $fee_args = array(); + + if ( isset( $fee['name'] ) ) { + $fee_args['name'] = $fee['name']; + } + + if ( isset( $fee['tax_class'] ) ) { + $fee_args['tax_class'] = $fee['tax_class']; + } + + if ( isset( $fee['total'] ) ) { + $fee_args['line_total'] = floatval( $fee['total'] ); + } + + if ( isset( $fee['total_tax'] ) ) { + $fee_args['line_tax'] = floatval( $fee['total_tax'] ); + } + + $fee_id = $order->update_fee( $fee['id'], $fee_args ); + + if ( ! $fee_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon. + * + * @param WC_Order $order Order data. + * @param array $coupon Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function set_coupon( $order, $coupon, $action ) { + // Coupon discount must be positive float. + if ( isset( $coupon['discount'] ) && 0 > floatval( $coupon['discount'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_total', __( 'Coupon discount must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + // Coupon code is required. + if ( empty( $coupon['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $coupon_id = $order->add_coupon( $coupon['code'], floatval( $coupon['discount'] ) ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create_order_coupon', __( 'Cannot create coupon, try again.', 'woocommerce' ), 500 ); + } + + } else { + $coupon_args = array(); + + if ( isset( $coupon['code'] ) ) { + $coupon_args['code'] = $coupon['code']; + } + + if ( isset( $coupon['discount'] ) ) { + $coupon_args['discount_amount'] = floatval( $coupon['discount'] ); + } + + $coupon_id = $order->update_coupon( $coupon['id'], $coupon_args ); + + if ( ! $coupon_id ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Helper method to add/update meta data, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @param int $order_id Order ID. + * @param array $meta_data Meta data in array( 'meta_key' => 'meta_value' ) format. + */ + protected function update_meta_data( $order_id, $meta_data ) { + foreach ( $meta_data as $meta_key => $meta_value ) { + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * 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'] ) ) { + 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; + } + + // Clear transients. + wc_delete_shop_order_transients( $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 object $post Inserted object (not a WP_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; + } + + /** + * 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 $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = 'set_' . $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( $item['id'] ), + absint( $order->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 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * 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', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @param WP_Post $post Post data. + * @return int|WP_Error + */ + protected function update_order( $request, $post ) { + try { + $update_totals = false; + $order = wc_get_order( $post ); + $order_args = array( 'order_id' => $order->id ); + + // Customer note. + if ( isset( $request['customer_note'] ) ) { + $order_args['customer_note'] = $request['customer_note']; + } + + // Customer ID. + if ( isset( $request['customer_id'] ) && $request['customer_id'] != $order->get_user_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 ); + } + + update_post_meta( $order->id, '_customer_user', $request['customer_id'] ); + } + + // Update addresses. + if ( is_array( $request['billing'] ) ) { + $this->update_address( $order, $request['billing'], 'billing' ); + } + if ( is_array( $request['shipping'] ) ) { + $this->update_address( $order, $request['shipping'], 'shipping' ); + } + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + if ( isset( $request[ $line ] ) && is_array( $request[ $line ] ) ) { + $update_totals = true; + foreach ( $request[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Set payment method. + if ( ! empty( $request['payment_method'] ) ) { + update_post_meta( $order->id, '_payment_method', $request['payment_method'] ); + } + if ( ! empty( $request['payment_method_title'] ) ) { + update_post_meta( $order->id, '_payment_method_title', $request['payment_method'] ); + } + if ( $order->needs_payment() && isset( $request['set_paid'] ) && true === $request['set_paid'] ) { + $order->payment_complete( ! empty( $request['transaction_id'] ) ? $request['transaction_id'] : '' ); + } + + // Set order currency. + if ( isset( $request['currency'] ) ) { + update_post_meta( $order->id, '_order_currency', $request['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update meta data. + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + $this->update_meta_data( $order->id, $request['meta_data'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $request['status'] ) ) { + $order->update_status( $request['status'], isset( $request['status_note'] ) ? $request['status_note'] : '' ); + } + + return $order->id; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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 ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->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, $post ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Clear transients. + wc_delete_shop_order_transients( $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 object $post Inserted object (not a WP_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 order statuses. + * + * @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; + } + + /** + * 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 when the order was made.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'Shows if 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, in the site's timezone.", '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, + ), + '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' ), + ), + '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 has been 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' => 'string', + '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' => 'integer', + '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' => 'integer', + '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' => 'string', + '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' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method 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, + ), + ), + ), + ), + ), + ), + ), + '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' => 'string', + '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' ), + ), + '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' => 'string', + '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' => 2, + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php b/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 00000000000..c82b1de93b9 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,240 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce/API + * @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[\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[\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_woocommerce_term_meta( $item->term_id, 'order_' . $this->taxonomy ); + + $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_woocommerce_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 ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-attributes-controller.php b/includes/api/v1/class-wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..95e0564242e --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,653 @@ +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[\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 doesn't 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 doesn't 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 doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you cannot delete 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 + */ + 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 manipule this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all attributes. + * + * @param WP_REST_Request $request + * @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; + } + + return rest_ensure_response( $data ); + } + + /** + * 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; + + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => ! empty( $request['type'] ) ? $request['type'] : 'select', + 'attribute_orderby' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', + 'attribute_public' => true === $request['has_archives'], + ); + + // Set the attribute slug. + if ( empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = wc_sanitize_taxonomy_name( stripslashes( $args['attribute_label'] ) ); + } else { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + } + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], true ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for errors. + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', $insert->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $wpdb->insert_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 ) ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + 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 ) { + global $wpdb; + + $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']; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + $args = array( + 'attribute_label' => $request['name'], + 'attribute_name' => $request['slug'], + 'attribute_type' => $request['type'], + 'attribute_orderby' => $request['order_by'], + 'attribute_public' => $request['has_archives'], + ); + + $i = 0; + foreach ( $args as $key => $value ) { + if ( empty( $value ) && ! is_bool( $value ) ) { + unset( $args[ $key ] ); + unset( $format[ $i ] ); + } + + $i++; + } + + // Set the attribute slug. + if ( ! empty( $args['attribute_name'] ) ) { + $args['attribute_name'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $args['attribute_name'] ) ) ); + + $valid_slug = $this->validate_attribute_slug( $args['attribute_name'], false ); + if ( is_wp_error( $valid_slug ) ) { + return $valid_slug; + } + } + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $args, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + // Checks for errors. + if ( false === $update ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Could not edit the attribute', 'woocommerce' ), 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 ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + 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 ) { + 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', __( '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 = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $attribute->attribute_id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute->attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * 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 ); + + // Fires woocommerce_attribute_deleted hook. + do_action( 'woocommerce_attribute_deleted', $attribute->attribute_id, $attribute->attribute_name, $taxonomy ); + + // Clear transients. + flush_rewrite_rules(); + delete_transient( 'wc_attribute_taxonomies' ); + + return $response; + } + + /** + * Prepare a single product attribute 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->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 ) { + if ( '' !== $this->attribute ) { + return $this->attribute; + } + + if ( $request['id'] ) { + $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); + + $this->attribute = $name; + } + + return $this->attribute; + } + + /** + * 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 doesn't exist.", 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $attribute; + } + + /** + * Validate attribute slug. + * + * @param string $slug + * @param bool $new_data + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max).', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + return true; + } +} diff --git a/includes/api/v1/class-wc-rest-product-categories-controller.php b/includes/api/v1/class-wc-rest-product-categories-controller.php new file mode 100644 index 00000000000..b6694afb2d9 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-categories-controller.php @@ -0,0 +1,265 @@ +term_id, 'display_type' ); + + // Get category order. + $menu_order = get_woocommerce_term_meta( $item->term_id, 'order' ); + + $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' => array(), + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + if ( $image_id = get_woocommerce_term_meta( $item->term_id, 'thumbnail_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 + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_woocommerce_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_woocommerce_term_meta( $id, 'order', $request['menu_order'] ); + } + + if ( ! empty( $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 = absint( $request['image']['id'] ); + } + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_woocommerce_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'] ) ) ); + } + } + } + + 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' ), + ), + '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 ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-reviews-controller.php b/includes/api/v1/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..f581757912e --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,331 @@ +/reviews endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Products controller class. + * + * @package WooCommerce/API + * @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[\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[\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_post_permissions( 'product', '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 webhook develivery. + * + * @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['product_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'product', '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; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $product = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + 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 = get_post( (int) $request['product_id'] ); + + if ( empty( $product->post_type ) || 'product' !== $product->post_type ) { + 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 ) !== intval( $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; + } + + /** + * 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 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[\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' ), + '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, + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + '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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php b/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 00000000000..d28e8e6357f --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,134 @@ + (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 ); + } +} diff --git a/includes/api/v1/class-wc-rest-product-tags-controller.php b/includes/api/v1/class-wc-rest-product-tags-controller.php new file mode 100644 index 00000000000..be33c4f1000 --- /dev/null +++ b/includes/api/v1/class-wc-rest-product-tags-controller.php @@ -0,0 +1,134 @@ + (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 ); + } +} diff --git a/includes/api/v1/class-wc-rest-products-controller.php b/includes/api/v1/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..749ea37629e --- /dev/null +++ b/includes/api/v1/class-wc-rest-products-controller.php @@ -0,0 +1,2761 @@ +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[\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 + * @param WP_REST_Request $request + * @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 ] ) ) { + $terms = explode( ',', $request[ $key ] ); + + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $terms = explode( ',', $request['type'] ); + + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $terms, + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names() ) ) { + $terms = explode( ',', $request['attribute_term'] ); + + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $terms, + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $request['sku'], + 'compare' => '=' + ); + } + + // 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'] = $this->get_post_types(); + } 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 + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_files() 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 + * @param string $taxonomy + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wp_get_post_terms( $product->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 + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + if ( $product->is_type( 'variation' ) ) { + if ( has_post_thumbnail( $product->get_variation_id() ) ) { + // Add variation image if set. + $attachment_ids[] = get_post_thumbnail_id( $product->get_variation_id() ); + } elseif ( has_post_thumbnail( $product->id ) ) { + // Otherwise use the parent product featured image if set. + $attachment_ids[] = get_post_thumbnail_id( $product->id ); + } + } else { + // Add featured image. + if ( has_post_thumbnail( $product->id ) ) { + $attachment_ids[] = get_post_thumbnail_id( $product->id ); + } + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_attachment_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 + * @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 + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) get_post_meta( $product->id, '_default_attributes', true ), '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' => str_replace( 'pa_', '', $key ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @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 + * @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' => str_replace( 'pa_', '', $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->id, $attribute ), + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => str_replace( 'pa_', '', $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->id, $attribute ), + ); + } + } + } + + return $attributes; + } + + /** + * Get product menu order. + * + * @param WC_Product $product + * @return int + */ + protected function get_product_menu_order( $product ) { + $menu_order = $product->get_post_data()->menu_order; + + if ( $product->is_type( 'variation' ) ) { + $variation = get_post( $product->get_variation_id() ); + $menu_order = $variation->menu_order; + } + + return $menu_order; + } + + /** + * Get product data. + * + * @param WC_Product $product + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, + 'name' => $product->get_title(), + 'slug' => $product->get_post_data()->post_name, + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'type' => $product->product_type, + 'status' => $product->get_post_data()->post_status, + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->visibility, + 'description' => wpautop( do_shortcode( $product->get_post_data()->post_content ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_post_data()->post_excerpt ), + '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->sale_price_dates_from ? date( 'Y-m-d', $product->sale_price_dates_from ) : '', + 'date_on_sale_to' => $product->sale_price_dates_to ? date( 'Y-m-d', $product->sale_price_dates_to ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => (int) get_post_meta( $product->id, 'total_sales', true ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => '' !== $product->download_limit ? (int) $product->download_limit : -1, + 'download_expiry' => '' !== $product->download_expiry ? (int) $product->download_expiry : -1, + 'download_type' => $product->download_type ? $product->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->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' => (int) $product->get_shipping_class_id(), + 'reviews_allowed' => ( 'open' === $product->get_post_data()->comment_status ), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => (int) $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( $product->get_related() ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsells() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sells() ), + 'parent_id' => $product->is_type( 'variation' ) ? $product->parent->id : $product->get_post_data()->post_parent, + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->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' => $this->get_product_menu_order( $product ), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product + * @return array + */ + protected function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = $product->get_child( $child_id ); + if ( ! $variation->exists() ) { + continue; + } + + $post_data = get_post( $variation->get_variation_id() ); + + $variations[] = array( + 'id' => $variation->get_variation_id(), + 'date_created' => wc_rest_prepare_date_response( $post_data->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post_data->post_modified_gmt ), + '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->sale_price_dates_from ? date( 'Y-m-d', $variation->sale_price_dates_from ) : '', + 'date_on_sale_to' => $variation->sale_price_dates_to ? date( 'Y-m-d', $variation->sale_price_dates_to ) : '', + '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->download_limit ? (int) $variation->download_limit : -1, + 'download_expiry' => '' !== $variation->download_expiry ? (int) $variation->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->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 $data + */ + 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. + * @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->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $product->is_type( 'variation' ) && $product->parent ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->parent->id ) ), + ); + } elseif ( $product->is_type( 'simple' ) && ! empty( $product->post->post_parent ) ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->post->post_parent ) ), + ); + } + + 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 ) { + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $data->post_title = wp_filter_post_kses( $request['name'] ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $data->post_content = wp_filter_post_kses( $request['description'] ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['short_description'] ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $data->post_status = get_post_status_object( $request['status'] ) ? $request['status'] : 'draft'; + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $data->post_name = $request['slug']; + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $data->menu_order = (int) $request['menu_order']; + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $data->comment_status = $request['reviews_allowed'] ? 'open' : 'closed'; + } + + // Only when creating products. + if ( empty( $request['id'] ) ) { + // Post type. + $data->post_type = $this->post_type; + + // 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 ); + } + + /** + * Save product images. + * + * @param int $product_id + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product_id, $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 ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product_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'] ) ) { + set_post_thumbnail( $product_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 ) ) { + update_post_meta( $product_id, '_product_image_gallery', implode( ',', $gallery ) ); + } + } else { + delete_post_meta( $product_id, '_thumbnail_id' ); + update_post_meta( $product_id, '_product_image_gallery', '' ); + } + } + + /** + * Save product shipping data. + * + * @param int $product_id + * @param array $data + */ + private function save_product_shipping_data( $product_id, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + update_post_meta( $product_id, '_weight', '' ); + update_post_meta( $product_id, '_length', '' ); + update_post_meta( $product_id, '_width', '' ); + update_post_meta( $product_id, '_height', '' ); + } else { + if ( isset( $data['weight'] ) ) { + update_post_meta( $product_id, '_weight', '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + update_post_meta( $product_id, '_height', '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + update_post_meta( $product_id, '_width', '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + update_post_meta( $product_id, '_length', '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + wp_set_object_terms( $product_id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + } + } + + /** + * Save downloadable files. + * + * @param in $product_id + * @param array $downloads + * @param int $variation_id + */ + private function save_downloadable_files( $product_id, $downloads, $variation_id = 0 ) { + $files = array(); + + // File paths will be stored in an array keyed off md5(file path). + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( ! isset( $file['file'] ) ) { + continue; + } + + $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; + + if ( 0 === strpos( $file['file'], 'http' ) ) { + $file_url = esc_url_raw( $file['file'] ); + } else { + $file_url = wc_clean( $file['file'] ); + } + + $files[ md5( $file_url ) ] = array( + 'name' => $file_name, + 'file' => $file_url, + ); + } + + // Grant permission to any newly added files on any existing orders for this product prior to saving. + do_action( 'woocommerce_process_product_file_download_paths', $product_id, $variation_id, $files ); + + $id = ( 0 === $variation_id ) ? $product_id : $variation_id; + + update_post_meta( $id, '_downloadable_files', $files ); + } + + /** + * Save taxonomy terms. + * + * @param int $product_id + * @param array $terms + * @param string $taxonomy + * @return array + */ + protected function save_taxonomy_terms( $product_id, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + $term_ids = array_unique( array_map( 'intval', $term_ids ) ); + + wp_set_object_terms( $product_id, $term_ids, 'product_' . $taxonomy ); + + return $terms; + } + + /** + * Save product meta. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + global $wpdb; + + // Product Type. + $product_type = null; + if ( isset( $request['type'] ) ) { + $product_type = wc_clean( $request['type'] ); + wp_set_object_terms( $product->id, $product_type, 'product_type' ); + } else { + $_product_type = get_the_terms( $product->id, 'product_type' ); + if ( is_array( $_product_type ) ) { + $_product_type = current( $_product_type ); + $product_type = $_product_type->slug; + } + } + + // Default total sales. + add_post_meta( $product->id, 'total_sales', '0', true ); + + // Virtual. + if ( isset( $request['virtual'] ) ) { + update_post_meta( $product->id, '_virtual', true === $request['virtual'] ? 'yes' : 'no' ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + update_post_meta( $product->id, '_tax_status', wc_clean( $request['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + update_post_meta( $product->id, '_tax_class', wc_clean( $request['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + update_post_meta( $product->id, '_visibility', wc_clean( $request['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + update_post_meta( $product->id, '_purchase_note', wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + update_post_meta( $product->id, '_featured', true === $request['featured'] ? 'yes' : 'no' ); + } + + // Shipping data. + $this->save_product_shipping_data( $product->id, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $sku = get_post_meta( $product->id, '_sku', true ); + $new_sku = wc_clean( $request['sku'] ); + + if ( '' === $new_sku ) { + update_post_meta( $product->id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->id, $new_sku ); + if ( ! $unique_sku ) { + $sku_found = wc_get_product_id_by_sku( $sku ); + + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400, array( 'resource_id' => $sku_found ) ); + } else { + update_post_meta( $product->id, '_sku', $new_sku ); + } + } else { + update_post_meta( $product->id, '_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(); + } + + // Update post terms. + if ( taxonomy_exists( $attribute_name ) ) { + wp_set_object_terms( $product->id, $values, $attribute_name ); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attributes[ $attribute_name ] = array( + 'name' => $attribute_name, + 'value' => '', + 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => 1, + ); + } + + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); + + // Text based, separate by pipe. + } else { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attributes[ sanitize_title( $attribute_name ) ] = array( + 'name' => $attribute_name, + 'value' => $values, + 'position' => isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0', + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => 0, + ); + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + update_post_meta( $product->id, '_product_attributes', $attributes ); + } + + // Sales and prices. + if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + update_post_meta( $product->id, '_regular_price', '' ); + update_post_meta( $product->id, '_sale_price', '' ); + update_post_meta( $product->id, '_sale_price_dates_from', '' ); + update_post_meta( $product->id, '_sale_price_dates_to', '' ); + update_post_meta( $product->id, '_price', '' ); + + } else { + + // Regular Price + if ( isset( $request['regular_price'] ) ) { + $regular_price = ( '' === $request['regular_price'] ) ? '' : $request['regular_price']; + } else { + $regular_price = get_post_meta( $product->id, '_regular_price', true ); + } + + // Sale Price + if ( isset( $request['sale_price'] ) ) { + $sale_price = ( '' === $request['sale_price'] ) ? '' : $request['sale_price']; + } else { + $sale_price = get_post_meta( $product->id, '_sale_price', true ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $date_from = $request['date_on_sale_from']; + } else { + $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $date_to = $request['date_on_sale_to']; + } else { + $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $product->id, $regular_price, $sale_price, $date_from, $date_to ); + } + + // Product parent ID for groups. + $parent_id = 0; + if ( isset( $request['parent_id'] ) ) { + $parent_id = wp_update_post( array( 'ID' => $product->id, 'post_parent' => absint( $request['parent_id'] ) ) ); + } + + // Update parent if grouped so price sorting works and stays in sync with the cheapest child. + if ( $parent_id > 0 || 'grouped' === $product_type ) { + + $clear_parent_ids = array(); + + if ( $parent_id > 0 ) { + $clear_parent_ids[] = $parent_id; + } + + if ( 'grouped' === $product_type ) { + $clear_parent_ids[] = $product->id; + } + + if ( ! empty( $clear_parent_ids ) ) { + foreach ( $clear_parent_ids as $clear_id ) { + + $children_by_price = get_posts( array( + 'post_parent' => $clear_id, + 'orderby' => 'meta_value_num', + 'order' => 'asc', + 'meta_key' => '_price', + 'posts_per_page' => 1, + 'post_type' => 'product', + 'fields' => 'ids' + ) ); + + if ( $children_by_price ) { + foreach ( $children_by_price as $child ) { + $child_price = get_post_meta( $child, '_price', true ); + update_post_meta( $clear_id, '_price', $child_price ); + } + } + } + } + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + update_post_meta( $product->id, '_sold_individually', true === $request['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $product->id, '_stock_status', true ); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $manage_stock = ( true === $request['manage_stock'] ) ? 'yes' : 'no'; + update_post_meta( $product->id, '_manage_stock', $manage_stock ); + } else { + $manage_stock = get_post_meta( $product->id, '_manage_stock', true ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $backorders = $request['backorders']; + update_post_meta( $product->id, '_backorders', $backorders ); + } else { + $backorders = get_post_meta( $product->id, '_backorders', true ); + } + + if ( 'grouped' === $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } elseif ( 'external' === $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, 'instock' ); + } elseif ( 'yes' === $manage_stock ) { + update_post_meta( $product->id, '_backorders', $backorders ); + + // Stock status is always determined by children so sync later. + if ( 'variable' !== $product_type ) { + wc_update_product_stock_status( $product->id, $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + wc_update_product_stock( $product->id, wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $product->id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $product->id, wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', $backorders ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } + + } elseif ( 'variable' !== $product_type ) { + wc_update_product_stock_status( $product->id, $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; + } + } + + update_post_meta( $product->id, '_upsell_ids', $upsells ); + } else { + delete_post_meta( $product->id, '_upsell_ids' ); + } + } + + // 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; + } + } + + update_post_meta( $product->id, '_crosssell_ids', $crosssells ); + } else { + delete_post_meta( $product->id, '_crosssell_ids' ); + } + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $this->save_taxonomy_terms( $product->id, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $this->save_taxonomy_terms( $product->id, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $is_downloadable = true === $request['downloadable'] ? 'yes' : 'no'; + update_post_meta( $product->id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $product->id, '_downloadable', true ); + } + + // Downloadable options. + if ( 'yes' === $is_downloadable ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $this->save_downloadable_files( $product->id, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + update_post_meta( $product->id, '_download_limit', -1 === $request['download_limit'] ? '' : absint( $request['download_limit'] ) ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + update_post_meta( $product->id, '_download_expiry', -1 === $request['download_expiry'] ? '' : absint( $request['download_expiry'] ) ); + } + + // Download type. + if ( isset( $request['download_type'] ) ) { + update_post_meta( $product->id, '_download_type', 'standard' === $request['download_type'] ? '' : wc_clean( $request['download_type'] ) ); + } + } + + // Product url and button text for external products. + if ( 'external' === $product_type ) { + if ( isset( $request['external_url'] ) ) { + update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); + } + + if ( isset( $request['button_text'] ) ) { + update_post_meta( $product->id, '_button_text', wc_clean( $request['button_text'] ) ); + } + } + + return true; + } + + /** + * Save variations. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_variations_data( $product, $request ) { + global $wpdb; + + $variations = $request['variations']; + $attributes = $product->get_attributes(); + + foreach ( $variations as $menu_order => $variation ) { + $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; + + // Generate a useful post title. + $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $product->id ) ) ); + + // Update or Add post. + if ( ! $variation_id ) { + $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; + + $new_variation = array( + 'post_title' => $variation_post_title, + 'post_content' => '', + 'post_status' => $post_status, + 'post_author' => get_current_user_id(), + 'post_parent' => $product->id, + 'post_type' => 'product_variation', + 'menu_order' => $menu_order, + ); + + $variation_id = wp_insert_post( $new_variation ); + + do_action( 'woocommerce_create_product_variation', $variation_id ); + } else { + $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); + if ( isset( $variation['visible'] ) ) { + $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; + $update_variation['post_status'] = $post_status; + } + + $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); + + do_action( 'woocommerce_update_product_variation', $variation_id ); + } + + // Stop with we don't have a variation ID. + if ( is_wp_error( $variation_id ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); + } + + // SKU. + if ( isset( $variation['sku'] ) ) { + $sku = get_post_meta( $variation_id, '_sku', true ); + $new_sku = wc_clean( $variation['sku'] ); + + if ( '' === $new_sku ) { + update_post_meta( $variation_id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + update_post_meta( $variation_id, '_sku', $new_sku ); + } + } else { + update_post_meta( $variation_id, '_sku', '' ); + } + } + } + + // Thumbnail. + if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { + $image = current( $variation['image'] ); + if ( $image && is_array( $image ) ) { + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->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'] ) ); + } + + update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); + } + } else { + delete_post_meta( $variation_id, '_thumbnail_id' ); + } + } + + // Virtual variation. + if ( isset( $variation['virtual'] ) ) { + $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_virtual', $is_virtual ); + } + + // Downloadable variation. + if ( isset( $variation['downloadable'] ) ) { + $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); + } + + // Shipping data. + $this->save_product_shipping_data( $variation_id, $variation ); + + // Stock handling. + if ( isset( $variation['manage_stock'] ) ) { + $manage_stock = ( true === $variation['manage_stock'] ) ? 'yes' : 'no'; + } else { + $manage_stock = get_post_meta( $variation_id, '_manage_stock', true ); + } + + update_post_meta( $variation_id, '_manage_stock', '' === $manage_stock ? 'no' : $manage_stock ); + + if ( isset( $variation['in_stock'] ) ) { + $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $variation_id, '_stock_status', true ); + } + + wc_update_product_stock_status( $variation_id, '' === $stock_status ? 'instock' : $stock_status ); + + if ( 'yes' === $manage_stock ) { + $backorders = get_post_meta( $variation_id, '_backorders', true ); + + if ( isset( $variation['backorders'] ) ) { + $backorders = $variation['backorders']; + } + + update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); + + if ( isset( $variation['stock_quantity'] ) ) { + wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); + } + } else { + delete_post_meta( $variation_id, '_backorders' ); + delete_post_meta( $variation_id, '_stock' ); + } + + // Regular Price. + if ( isset( $variation['regular_price'] ) ) { + $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; + } else { + $regular_price = get_post_meta( $variation_id, '_regular_price', true ); + } + + // Sale Price. + if ( isset( $variation['sale_price'] ) ) { + $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; + } else { + $sale_price = get_post_meta( $variation_id, '_sale_price', true ); + } + + if ( isset( $variation['date_on_sale_from'] ) ) { + $date_from = $variation['date_on_sale_from']; + } else { + $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $variation['date_on_sale_to'] ) ) { + $date_to = $variation['date_on_sale_to']; + } else { + $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); + + // Tax class. + if ( isset( $variation['tax_class'] ) ) { + if ( $variation['tax_class'] !== 'parent' ) { + update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); + } else { + delete_post_meta( $variation_id, '_tax_class' ); + } + } + + // Downloads. + if ( 'yes' === $is_downloadable ) { + // Downloadable files. + if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { + $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); + } + + // Download limit. + if ( isset( $variation['download_limit'] ) ) { + update_post_meta( $variation_id, '_download_limit', -1 === $variation['download_limit'] ? '' : absint( $variation['download_limit'] ) ); + } + + // Download expiry. + if ( isset( $variation['download_expiry'] ) ) { + update_post_meta( $variation_id, '_download_expiry', -1 === $variation['download_expiry'] ? '' : absint( $variation['download_expiry'] ) ); + } + } else { + update_post_meta( $variation_id, '_download_limit', '' ); + update_post_meta( $variation_id, '_download_expiry', '' ); + update_post_meta( $variation_id, '_downloadable_files', '' ); + } + + // Description. + if ( isset( $variation['description'] ) ) { + update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); + } + + // Update taxonomies. + if ( isset( $variation['attributes'] ) ) { + $updated_attribute_keys = array(); + + foreach ( $variation['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 ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); + $updated_attribute_keys[] = $_attribute_key; + $attribute_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', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + update_post_meta( $variation_id, $_attribute_key, $attribute_value ); + } + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $variation_id, $key ); + } + } + + do_action( 'woocommerce_rest_save_product_variation', $variation_id, $menu_order, $variation ); + } + + // Update parent if variable so price sorting works and stays in sync with the cheapest child. + WC_Product_Variable::sync( $product->id ); + + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_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; + } + } + } + } + + update_post_meta( $product->id, '_default_attributes', $default_attributes ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product->id, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( isset( $request['type'] ) && 'variable' === $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product->id, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } else { + // Just sync variations. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + } + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + // Delete product attachments. + $attachments = get_children( array( + 'post_parent' => $post->ID, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + wp_delete_post( $post->ID, 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 ); + + if ( empty( $id ) || empty( $post->ID ) || ! in_array( $post->post_type, $this->get_post_types() ) ) { + 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 ) ) { + 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 ) { + $child_product_variations = get_children( 'post_parent=' . $id . '&post_type=product_variation' ); + + if ( ! empty( $child_product_variations ) ) { + foreach ( $child_product_variations as $child ) { + wp_delete_post( $child->ID, true ); + } + } + + $child_products = get_children( 'post_parent=' . $id . '&post_type=product' ); + + if ( ! empty( $child_products ) ) { + foreach ( $child_products as $child ) { + $child_post = array(); + $child_post['ID'] = $child->ID; + $child_post['post_parent'] = 0; + wp_update_post( $child_post ); + } + } + + $result = wp_delete_post( $id, true ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + 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 ) { + 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 ) { + 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 = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $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_keys( get_post_statuses() ), + '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 data 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 MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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' => __( 'Amount of times the product can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the product.', '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', 'application', 'music' ), + '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( + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + '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' ), + ), + '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_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 data 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 MD5 hash.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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' => __( 'Amount of times the variation can be downloaded.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days that the customer has up to be able to download the variation.', '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( + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + '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_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', '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' ), 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.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + '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 (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + '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; + } +} diff --git a/includes/api/v1/class-wc-rest-report-sales-controller.php b/includes/api/v1/class-wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..539b6a6efdb --- /dev/null +++ b/includes/api/v1/class-wc-rest-report-sales-controller.php @@ -0,0 +1,398 @@ +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( + 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_max' => array( + 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-AA' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-report-top-sellers-controller.php b/includes/api/v1/class-wc-rest-report-top-sellers-controller.php new file mode 100644 index 00000000000..7c27a670daf --- /dev/null +++ b/includes/api/v1/class-wc-rest-report-top-sellers-controller.php @@ -0,0 +1,174 @@ + $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_title(), + '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 ); + } +} diff --git a/includes/api/v1/class-wc-rest-reports-controller.php b/includes/api/v1/class-wc-rest-reports-controller.php new file mode 100644 index 00000000000..c8211d44966 --- /dev/null +++ b/includes/api/v1/class-wc-rest-reports-controller.php @@ -0,0 +1,174 @@ +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 all reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + + 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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-tax-classes-controller.php b/includes/api/v1/class-wc-rest-tax-classes-controller.php new file mode 100644 index 00000000000..396104d99cb --- /dev/null +++ b/includes/api/v1/class-wc-rest-tax-classes-controller.php @@ -0,0 +1,362 @@ +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\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 boolean + */ + 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 boolean + */ + 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 + * @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. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $exists = false; + $classes = WC_Tax::get_tax_classes(); + $tax_class = array( + 'slug' => sanitize_title( $request['name'] ), + 'name' => $request['name'], + ); + + // Check if class exists. + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $exists = true; + break; + } + } + + // Return error if tax class already exists. + if ( $exists ) { + return new WP_Error( 'woocommerce_rest_tax_class_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Add the new class. + $classes[] = $tax_class['name']; + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + $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 = array( + 'slug' => sanitize_title( $request['slug'] ), + 'name' => '', + ); + $classes = WC_Tax::get_tax_classes(); + $deleted = false; + + foreach ( $classes as $key => $class ) { + if ( sanitize_title( $class ) === $tax_class['slug'] ) { + $tax_class['name'] = $class; + unset( $classes[ $key ] ); + $deleted = true; + break; + } + } + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( 'woocommerce_tax_classes', implode( "\n", $classes ) ); + + // Delete tax rate locations locations from the selected class. + $wpdb->query( $wpdb->prepare( " + DELETE locations.* + FROM {$wpdb->prefix}woocommerce_tax_rate_locations AS locations + INNER JOIN + {$wpdb->prefix}woocommerce_tax_rates AS rates + ON rates.tax_rate_id = locations.tax_rate_id + WHERE rates.tax_rate_class = '%s' + ", $tax_class['slug'] ) ); + + // Delete tax rates in the selected class. + $wpdb->delete( $wpdb->prefix . 'woocommerce_tax_rates', array( 'tax_rate_class' => $tax_class['slug'] ), array( '%s' ) ); + + $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 Request object. + * @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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-taxes-controller.php b/includes/api/v1/class-wc-rest-taxes-controller.php new file mode 100644 index 00000000000..63950ea62f4 --- /dev/null +++ b/includes/api/v1/class-wc-rest-taxes-controller.php @@ -0,0 +1,709 @@ +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[\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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 boolean + */ + 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 manipule 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['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' => 'tax_rate_id', + 'order' => 'tax_rate_order', + ); + $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 ); + + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class. + if ( ! empty( $prepared_args['class'] ) ) { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + $query .= " AND tax_rate_class = '$class'"; + } + + // Order tax rates. + $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); + + // Pagination. + $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); + + // Query taxes. + $results = $wpdb->get_results( $query . $order_by . $pagination ); + + $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 ); + + // Store pagation values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + + // Calcule totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $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. + * + * @todo Replace with CRUD in 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @param stdClass|null $current Existing tax object. + * @return stdClass + */ + 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 ) { + WC_Tax::_update_tax_rate( $id, $data ); + } else { + $id = WC_Tax::_insert_tax_rate( $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 ) { + global $wpdb; + + $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', + ); + + // 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 + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + $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; + } + + /** + * 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' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + '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 = 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' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + ), + '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' ), array_map( 'sanitize_title', WC_Tax::get_tax_classes() ) ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/api/v1/class-wc-rest-webhook-deliveries.php b/includes/api/v1/class-wc-rest-webhook-deliveries.php new file mode 100644 index 00000000000..a88b65b3f9b --- /dev/null +++ b/includes/api/v1/class-wc-rest-webhook-deliveries.php @@ -0,0 +1,329 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce/API + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @package WooCommerce/API + * @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[\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[\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 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_post_permissions( 'shop_webhook', '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 webhook develivery. + * + * @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['webhook_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'shop_webhook', '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; + } + + /** + * Get all webhook deliveries. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $webhook = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = $webhook->get_delivery_logs(); + + $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 = new WC_Webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook->post_data->post_type ) || 'shop_webhook' !== $webhook->post_data->post_type ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = $webhook->get_delivery_log( $id ); + + 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; + + // Add timestamp. + $data['date_created'] = wc_rest_prepare_date_response( $log->comment->comment_date_gmt ); + + // Remove comment object. + unset( $data['comment'] ); + + $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[\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' => __( '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' ) ), + ); + } +} diff --git a/includes/api/v1/class-wc-rest-webhooks-controller.php b/includes/api/v1/class-wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..02625d8fe7c --- /dev/null +++ b/includes/api/v1/class-wc-rest-webhooks-controller.php @@ -0,0 +1,576 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * 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' ), + ), + 'secret' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook secret.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\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' ), + ) ); + } + + /** + * 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'] ) ) { + 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; + } + + $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; + + $webhook = new WC_Webhook( $post_id ); + + // Set topic. + $webhook->set_topic( $request['topic'] ); + + // Set delivery URL. + $webhook->set_delivery_url( $request['delivery_url'] ); + + // Set secret. + $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : '' ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $post = get_post( $post_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 Inserted 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 ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + 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']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $webhook = new WC_Webhook( $id ); + + // 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'] ) ) { + $webhook->update_status( $request['status'] ); + } + + $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 ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Inserted 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 ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + 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 ) ); + } + + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post id.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + $result = wp_delete_post( $id, true ); + + if ( ! $result ) { + 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 ); + + // Clear cache. + delete_transient( 'woocommerce_webhook_ids' ); + + 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 ) { + global $wpdb; + + $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' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $password = strlen( uniqid( 'webhook_' ) ); + $data->post_password = $password > 20 ? substr( $password, 0, 20 ) : $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 WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $post, $request ) { + $id = (int) $post->ID; + $webhook = new WC_Webhook( $id ); + $data = array( + 'id' => $webhook->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_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_post_data()->post_modified_gmt ), + ); + + $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( $post ) ); + + /** + * 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 ); + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + switch ( $request['status'] ) { + case 'active' : + $args['post_status'] = 'publish'; + break; + case 'paused' : + $args['post_status'] = 'draft'; + break; + case 'disabled' : + $args['post_status'] = 'pending'; + break; + default : + $args['post_status'] = 'any'; + break; + } + + return $args; + } + + /** + * 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( 'active', 'paused', 'disabled' ), + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wc_is_webhook_valid_topic', + ), + ), + '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 is 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['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; + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 5b78374325d..4b77914c355 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -123,6 +123,7 @@ class WC_API extends WC_Legacy_API { /** * Include REST API classes. + * * @since 2.6.0 */ private function rest_api_includes() { @@ -144,7 +145,29 @@ class WC_API extends WC_Legacy_API { include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-terms-controller.php' ); include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-settings-api.php' ); - // REST API controllers. + // REST API v1 controllers. + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-coupons-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-customer-downloads-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-customers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-order-notes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-order-refunds-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-orders-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-attribute-terms-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-attributes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-categories-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-reviews-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-shipping-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-product-tags-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-products-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-report-sales-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-report-top-sellers-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-reports-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-tax-classes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-taxes-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhook-deliveries.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhooks-controller.php' ); + + // REST API v2 controllers. include_once( dirname( __FILE__ ) . '/api/class-wc-rest-coupons-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-customer-downloads-controller.php' ); include_once( dirname( __FILE__ ) . '/api/class-wc-rest-customers-controller.php' ); @@ -186,6 +209,29 @@ class WC_API extends WC_Legacy_API { $this->register_wp_admin_settings(); $controllers = array( + // v1 controllers. + 'WC_REST_Coupons_V1_Controller', + 'WC_REST_Customer_Downloads_V1_Controller', + 'WC_REST_Customers_V1_Controller', + 'WC_REST_Order_Notes_V1_Controller', + 'WC_REST_Order_Refunds_V1_Controller', + 'WC_REST_Orders_V1_Controller', + 'WC_REST_Product_Attribute_Terms_V1_Controller', + 'WC_REST_Product_Attributes_V1_Controller', + 'WC_REST_Product_Categories_V1_Controller', + 'WC_REST_Product_Reviews_V1_Controller', + 'WC_REST_Product_Shipping_Classes_V1_Controller', + 'WC_REST_Product_Tags_V1_Controller', + 'WC_REST_Products_V1_Controller', + 'WC_REST_Report_Sales_V1_Controller', + 'WC_REST_Report_Top_Sellers_V1_Controller', + 'WC_REST_Reports_V1_Controller', + 'WC_REST_Tax_Classes_V1_Controller', + 'WC_REST_Taxes_V1_Controller', + 'WC_REST_Webhook_Deliveries_V1_Controller', + 'WC_REST_Webhooks_V1_Controller', + + // v2 controllers. 'WC_REST_Coupons_Controller', 'WC_REST_Customer_Downloads_Controller', 'WC_REST_Customers_Controller',