From 7a52ec465b61b0da658c0d04daba1093fe08d298 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 18:05:06 -0200 Subject: [PATCH 01/16] Fixed docblocks and removed dead variables --- includes/api/class-wc-rest-order-refunds-controller.php | 2 +- includes/api/class-wc-rest-webhooks-controller.php | 2 +- includes/api/v1/class-wc-rest-coupons-controller.php | 4 ---- includes/api/v1/class-wc-rest-order-refunds-controller.php | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index a05c044980f..d41d71fd63f 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Order Refunds controller class. * * @package WooCommerce/API - * @extends WC_REST_Posts_Controller + * @extends WC_REST_Orders_Controller */ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { diff --git a/includes/api/class-wc-rest-webhooks-controller.php b/includes/api/class-wc-rest-webhooks-controller.php index ab0b151d59b..d5551c66bc2 100644 --- a/includes/api/class-wc-rest-webhooks-controller.php +++ b/includes/api/class-wc-rest-webhooks-controller.php @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Webhooks controller class. * * @package WooCommerce/API - * @extends WC_REST_Posts_Controller + * @extends WC_REST_Webhooks_V1_Controller */ class WC_REST_Webhooks_Controller extends WC_REST_Webhooks_V1_Controller { diff --git a/includes/api/v1/class-wc-rest-coupons-controller.php b/includes/api/v1/class-wc-rest-coupons-controller.php index e18a7305b0b..2a26cc4f7ba 100644 --- a/includes/api/v1/class-wc-rest-coupons-controller.php +++ b/includes/api/v1/class-wc-rest-coupons-controller.php @@ -131,8 +131,6 @@ class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller { * @return array */ public function query_args( $args, $request ) { - global $wpdb; - if ( ! empty( $request['code'] ) ) { $id = wc_get_coupon_id_by_code( $request['code'] ); $args['post__in'] = array( $id ); @@ -232,8 +230,6 @@ class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller { * @return WP_Error|stdClass $data Post object. */ protected function prepare_item_for_database( $request ) { - global $wpdb; - $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $coupon = new WC_Coupon( $id ); $schema = $this->get_item_schema(); diff --git a/includes/api/v1/class-wc-rest-order-refunds-controller.php b/includes/api/v1/class-wc-rest-order-refunds-controller.php index c4d1124caf0..c6b0bc0f7e6 100644 --- a/includes/api/v1/class-wc-rest-order-refunds-controller.php +++ b/includes/api/v1/class-wc-rest-order-refunds-controller.php @@ -18,7 +18,7 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Order Refunds controller class. * * @package WooCommerce/API - * @extends WC_REST_Posts_Controller + * @extends WC_REST_Orders_V1_Controller */ class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { From d4ce22cb76b962f63b721115f8768cc3b113c848 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 20:27:07 -0200 Subject: [PATCH 02/16] Created WC_REST_CRUD_Controller --- .../abstract-wc-rest-crud-controller.php | 569 ++++++++++++++++++ includes/class-wc-api.php | 1 + 2 files changed, 570 insertions(+) create mode 100644 includes/abstracts/abstract-wc-rest-crud-controller.php diff --git a/includes/abstracts/abstract-wc-rest-crud-controller.php b/includes/abstracts/abstract-wc-rest-crud-controller.php new file mode 100644 index 00000000000..adbd125300b --- /dev/null +++ b/includes/abstracts/abstract-wc-rest-crud-controller.php @@ -0,0 +1,569 @@ +post_type, 'read', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'edit', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', (int) $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get object. + * + * @param int $id Object ID. + * @return WP_Error|WC_Data + */ + protected function get_object( $id ) { + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get object permalink. + * + * @param int $id Object ID. + * @return string + */ + protected function get_permalink( $object ) { + return ''; + } + + /** + * Prepares the object for the REST response. + * + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + protected function prepare_object_for_response( $object, $request ) { + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepares one object for create or update operation. + * + * @since 2.7.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Save an object data. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $object->save(); + + return $object; + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, true ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) ); + + return $response; + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $object = $this->save_object( $request, false ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + + $args['date_query'] = array(); + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $request['before'] ) ) { + $args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $request['after'] ) ) { + $args['date_query'][0]['after'] = $request['after']; + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); + + return $this->prepare_items_query( $args, $request ); + } + + /** + * Get objects. + * + * @since 2.7.0 + * @param array $query_args Query args. + * @return array + */ + protected function get_objects( $query_args ) { + $query = new WP_Query(); + $result = $query->query( $query_args ); + + $total_posts = $query->found_posts; + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + return array( + 'objects' => array_map( array( $this, 'get_object' ), $result ), + 'total' => (int) $total_posts, + 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), + ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $query_args = $this->prepare_objects_query( $request ); + $query_results = $this->get_objects( $query_args ); + + $objects = array(); + foreach ( $query_results['objects'] as $object ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + continue; + } + + $data = $this->prepare_object_for_response( $object, $request ); + $objects[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $max_pages = $query_results['pages']; + + $response = rest_ensure_response( $objects ); + $response->header( 'X-WP-Total', $query_results['total'] ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = 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; + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + + if ( 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $result = $object->delete( true ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) && 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $result = $object->delete(); + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['search'] = array( + 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( $this->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + return $params; + } +} diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index 9afe97028f1..fc5ec890855 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -138,6 +138,7 @@ class WC_API extends WC_Legacy_API { // Abstract controllers. include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-controller.php' ); include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-posts-controller.php' ); + include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-crud-controller.php' ); include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-terms-controller.php' ); include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-rest-shipping-zones-controller.php' ); include_once( dirname( __FILE__ ) . '/abstracts/abstract-wc-settings-api.php' ); From 34cf70407e4c51db2447678cc18fc48b5343df50 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 20:31:36 -0200 Subject: [PATCH 03/16] Use CRUD Controller for coupons --- .../api/class-wc-rest-coupons-controller.php | 209 +++++++++++++++--- 1 file changed, 177 insertions(+), 32 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 245f398b6be..9ba0564d5a1 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Coupons controller class. * * @package WooCommerce/API - * @extends WC_REST_Coupons_V1_Controller + * @extends WC_REST_CRUD_Controller */ -class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { +class WC_REST_Coupons_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. @@ -29,16 +29,114 @@ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { */ protected $namespace = 'wc/v2'; + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'coupons'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_coupon'; + + /** + * Register the routes for coupons. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 2.7.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return new WC_Coupon( $id ); + } + /** * 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 + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response */ - public function prepare_item_for_response( $post, $request ) { - $coupon = new WC_Coupon( (int) $post->ID ); - $data = $coupon->get_data(); + public function prepare_object_for_response( $object, $request ) { + $data = $object->get_data(); $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); $format_date = array( 'date_created', 'date_modified', 'date_expires' ); @@ -63,40 +161,47 @@ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $post, $request ) ); + $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Only reutrn writeable props from schema. + * + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); } /** * Prepare a single coupon for create or update. * - * @param WP_REST_Request $request Request object. - * @return WP_Error|stdClass $data Post object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data */ - protected function prepare_item_for_database( $request ) { - global $wpdb; - + protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $coupon = new WC_Coupon( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); // Validate required POST fields. - if ( 'POST' === $request->get_method() && 0 === $coupon->get_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 ) ); - } + if ( $creating && empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); } // Handle all writable props. @@ -106,8 +211,8 @@ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { if ( ! is_null( $value ) ) { switch ( $key ) { case 'code' : - $coupon_code = apply_filters( 'woocommerce_coupon_code', $value ); - $id = $coupon->get_id() ? $coupon->get_id() : 0; + $coupon_code = apply_filters( 'woocommerce_coupon_code', $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); if ( $id_from_code ) { @@ -136,15 +241,37 @@ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { } /** - * Filter the query_vars used in `get_items` for the constructed query. + * Filters an object before it is inserted via the REST API. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for insertion. + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. * - * @param WC_Coupon $coupon The coupon object. - * @param WP_REST_Request $request Request object. + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); + } + + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; } /** @@ -331,4 +458,22 @@ class WC_REST_Coupons_Controller extends WC_REST_Coupons_V1_Controller { ); 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; + } } From 687433d5cc341707b81ebbbd47e12aa9b40b4967 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 21:15:34 -0200 Subject: [PATCH 04/16] Use CRUD Controller for orders --- .../api/class-wc-rest-coupons-controller.php | 42 +- .../api/class-wc-rest-orders-controller.php | 503 +++++++++++++++++- 2 files changed, 497 insertions(+), 48 deletions(-) diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index 9ba0564d5a1..f36d771b9b2 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -176,6 +176,27 @@ class WC_REST_Coupons_Controller extends WC_REST_CRUD_Controller { return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; + } + /** * Only reutrn writeable props from schema. * @@ -253,27 +274,6 @@ class WC_REST_Coupons_Controller extends WC_REST_CRUD_Controller { return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); } - /** - * Prepare objects query. - * - * @since 2.7.0 - * @param WP_REST_Request $request Full details about the request. - * @return array - */ - protected function prepare_objects_query( $request ) { - $args = parent::prepare_objects_query( $request ); - - if ( ! empty( $request['code'] ) ) { - $id = wc_get_coupon_id_by_code( $request['code'] ); - $args['post__in'] = array( $id ); - } - - // Get only ids. - $args['fields'] = 'ids'; - - return $args; - } - /** * Get the Coupon's schema, conforming to JSON Schema. * diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index fa1c205660c..b14b37c38ac 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Orders controller class. * * @package WooCommerce/API - * @extends WC_REST_Orders_V1_Controller + * @extends WC_REST_CRUD_Controller */ -class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { +class WC_REST_Orders_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. @@ -29,12 +29,111 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { */ protected $namespace = 'wc/v2'; + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order'; + + /** + * If object is hierarchical. + * + * @var bool + */ + protected $hierarchical = true; + /** * Stores the request. * @var array */ protected $request = array(); + /** + * Register the routes for orders. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 2.7.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_order( $id ); + } + /** * Expands an order item to get its data. * @param WC_Order_item $item @@ -88,15 +187,14 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { /** * 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 + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response */ - public function prepare_item_for_response( $post, $request ) { + public function prepare_object_for_response( $object, $request ) { $this->request = $request; - $statuses = wc_get_order_statuses(); - $order = wc_get_order( $post ); - $data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() ); + $data = array_merge( array( 'id' => $object->get_id() ), $object->get_data() ); $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); @@ -121,7 +219,7 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { // Refunds. $data['refunds'] = array(); - foreach ( $order->get_refunds() as $refund ) { + foreach ( $object->get_refunds() as $refund ) { $data['refunds'][] = array( 'id' => $refund->get_id(), 'refund' => $refund->get_reason() ? $refund->get_reason() : '', @@ -133,34 +231,136 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { $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( $order, $request ) ); + $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** - * Prepare a single order for create. + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $object->get_customer_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), + ); + } + + if ( 0 !== (int) $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + global $wpdb; + + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + if ( '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; + } + + /** + * Only reutrn writeable props from schema. + * + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. * * @param WP_REST_Request $request Request object. - * @return WP_Error|WC_Order $data Object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data */ - protected function prepare_item_for_database( $request ) { + protected function prepare_object_for_database( $request, $creating = false ) { $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; $order = new WC_Order( $id ); $schema = $this->get_item_schema(); $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); - // Handle all writable props + // Handle all writable props. foreach ( $data_keys as $key ) { $value = $request[ $key ]; @@ -203,19 +403,146 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { } /** - * Filter the data for the insert. + * Filters an object before it is inserted via the REST API. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. * - * @param WC_Order $order The prder object. - * @param WP_REST_Request $request Request object. + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Create base WC Order object. + * + * @deprecated 2.7.0 + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Save an object data. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + if ( $creating ) { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } + + $object->save(); + + // Actions for after the order is saved. + if ( $creating ) { + if ( true === $request['set_paid'] ) { + $object->payment_complete( $request['transaction_id'] ); + } + } else { + // Handle set paid. + if ( $object->needs_payment() && true === $request['set_paid'] ) { + $object->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'], $request['shipping'], $request['line_items'], $request['shipping_lines'], $request['fee_lines'], $request['coupon_lines'] ) ) { + $object->calculate_totals(); + } + } + + return $object; + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @param array $posted Request data + * @return int + */ + protected function get_product_id( $posted ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } } /** * Maybe set item meta if posted. + * * @param WC_Order_Item $item * @param array $posted Request data. */ @@ -323,6 +650,89 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { return $item; } + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 2.7.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + /** * Get the Order's schema, conforming to JSON Schema. * @@ -1126,4 +1536,43 @@ class WC_REST_Orders_Controller extends WC_REST_Orders_V1_Controller { 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; + } } From 197d2eb98e2d23bd0604e60d70c1b9613972c13b Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 21:45:12 -0200 Subject: [PATCH 05/16] Use CRUD Controller for order refunds --- ...class-wc-rest-order-refunds-controller.php | 145 ++++++++++-------- 1 file changed, 81 insertions(+), 64 deletions(-) diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index d41d71fd63f..f8e7947e261 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -53,8 +53,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { * 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 ); + add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' ); } /** @@ -119,13 +118,25 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { } /** - * Prepare a single order refund output for response. + * Get object. * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $data + * @since 2.7.0 + * @param int $id Object ID. + * @return WC_Data */ - public function prepare_item_for_response( $post, $request ) { + protected function get_object( $id ) { + return wc_get_order( $id ); + } + + /** + * Prepare a single order output for response. + * + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { $this->request = $request; $order = wc_get_order( (int) $request['order_id'] ); @@ -133,13 +144,11 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } - $refund = wc_get_order( $post ); - - if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { + if ( ! $object || $object->get_parent_id() !== $order->get_id() ) { return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); } - $data = $refund->get_data(); + $data = $object->get_data(); $format_decimal = array( 'amount' ); $format_date = array( 'date_created' ); $format_line_items = array( 'line_items' ); @@ -159,7 +168,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); } - // Unset unwanted data + // Unset unwanted data. unset( $data['parent_id'], $data['status'], $data['currency'], $data['prices_include_tax'], $data['version'], $data['date_modified'], $data['discount_total'], $data['discount_tax'], @@ -175,40 +184,39 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { // Wrap the data in a response object. $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $refund, $request ) ); + $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); } /** * Prepare links for the request. * - * @param WC_Order_Refund $refund Comment object. + * @param WC_Data $object Object data. * @param WP_REST_Request $request Request object. - * @return array Links for the given order refund. + * @return array Links for the given post. */ - protected function prepare_links( $refund, $request ) { - $order_id = $refund->get_parent_id(); - $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + protected function prepare_links( $object, $request ) { + $base = str_replace( '(?P[\d]+)', $object->get_parent_id(), $this->rest_base ); $links = array( 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), ), 'collection' => array( 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), ), 'up' => array( - 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), ), ); @@ -216,13 +224,15 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { } /** - * Query args. + * Prepare objects query. * - * @param array $args Request args. - * @param WP_REST_Request $request Request object. + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. * @return array */ - public function query_args( $args, $request ) { + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + $args['post_status'] = array_keys( wc_get_order_statuses() ); $args['post_parent__in'] = array( absint( $request['order_id'] ) ); @@ -230,21 +240,18 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { } /** - * Create a single item. + * Prepares one object for create or update operation. * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response + * @since 2.7.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); - } + protected function prepare_object_for_database( $request, $creating = false ) { + $order = wc_get_order( (int) $request['order_id'] ); - $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 ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); } if ( 0 > $request['amount'] ) { @@ -253,7 +260,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { // Create the refund. $refund = wc_create_refund( array( - 'order_id' => $order_data->ID, + 'order_id' => $order->get_id(), 'amount' => $request['amount'], 'reason' => empty( $request['reason'] ) ? null : $request['reason'], 'line_items' => $request['line_items'], @@ -269,25 +276,41 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); } - $post = get_post( $refund->get_id() ); - $this->update_additional_fields_for_object( $post, $request ); - /** - * Fires after a single item is created or updated via the REST API. + * Filters an object before it is inserted 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. + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. */ - do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } - $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 ) ) ); + /** + * Save an object data. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); - return $response; + if ( is_wp_error( $object ) ) { + return $object; + } + + return $object; + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } } /** @@ -495,13 +518,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { 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', - ); + unset( $params['status'], $params['customer'], $params['product'] ); return $params; } From 01a5296e7e5e3b16e0d6113571b515637129c19d Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 22:25:07 -0200 Subject: [PATCH 06/16] Fixed how CRUD delete items by REST API --- .../abstract-wc-rest-crud-controller.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-crud-controller.php b/includes/abstracts/abstract-wc-rest-crud-controller.php index adbd125300b..4cf52f0a300 100644 --- a/includes/abstracts/abstract-wc-rest-crud-controller.php +++ b/includes/abstracts/abstract-wc-rest-crud-controller.php @@ -373,6 +373,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { $id = (int) $request['id']; $force = (bool) $request['force']; $object = $this->get_object( (int) $request['id'] ); + $result = false; if ( 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); @@ -400,7 +401,8 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { // If we're forcing, then delete permanently. if ( $force ) { - $result = $object->delete( true ); + $object->delete( true ); + $result = 0 === $object->get_id(); } else { // If we don't support trashing for this type, error out. if ( ! $supports_trash ) { @@ -409,12 +411,15 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { } // Otherwise, only trash if we haven't already. - if ( is_callable( array( $object, 'get_status' ) ) && 'trash' === $object->get_status() ) { - /* translators: %s: post type */ - return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); - } + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } - $result = $object->delete(); + $object->delete(); + $result = 'trash' === $object->get_status(); + } } if ( ! $result ) { From 31f68377f9badb58d75852741051bb5290171d03 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 22:25:20 -0200 Subject: [PATCH 07/16] Fixed docblock --- includes/api/class-wc-rest-orders-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index b14b37c38ac..ccf5ed876e4 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -348,7 +348,7 @@ class WC_REST_Orders_Controller extends WC_REST_CRUD_Controller { } /** - * Prepare a single coupon for create or update. + * Prepare a single order for create or update. * * @param WP_REST_Request $request Request object. * @param bool $creating If is creating a new object. From 63cb7e7c27ba48a8060f7b3773ca3c0765cead83 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 22:26:21 -0200 Subject: [PATCH 08/16] Use CRUD Controller for products --- .../api/class-wc-rest-products-controller.php | 1178 ++++++++++++++++- 1 file changed, 1116 insertions(+), 62 deletions(-) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index c6ce9892433..803680b77ed 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -18,9 +18,9 @@ if ( ! defined( 'ABSPATH' ) ) { * REST API Products controller class. * * @package WooCommerce/API - * @extends WC_REST_Products_V1_Controller + * @extends WC_REST_CRUD_Controller */ -class WC_REST_Products_Controller extends WC_REST_Products_V1_Controller { +class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { /** * Endpoint namespace. @@ -30,13 +30,161 @@ class WC_REST_Products_Controller extends WC_REST_Products_V1_Controller { protected $namespace = 'wc/v2'; /** - * Query args. + * Route base. * - * @param array $args Request args. - * @param WP_REST_Request $request Request data. + * @var string + */ + protected $rest_base = 'products'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'product'; + + /** + * If object is hierarchical. + * + * @var bool + */ + protected $hierarchical = true; + + /** + * Initialize product actions. + */ + public function __construct() { + add_action( "woocommerce_rest_insert_{$this->post_type}_object", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get object. + * + * @since 2.7.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single product output for response. + * + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_product_data( $object ); + + // Add variations to variable products. + if ( $object->is_type( 'variable' ) && $object->has_child() ) { + $data['variations'] = $object->get_children(); + } + + // Add grouped products data. + if ( $object->is_type( 'grouped' ) && $object->has_child() ) { + $data['grouped_products'] = $object->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. * @return array */ - public function query_args( $args, $request ) { + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + // Set post_status. $args['post_status'] = $request['status']; @@ -154,46 +302,224 @@ class WC_REST_Products_Controller extends WC_REST_Products_V1_Controller { } /** - * Prepare a single product output for response. + * Get the downloads for a product or product variation. * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array */ - public function prepare_item_for_response( $post, $request ) { - $product = wc_get_product( $post ); - $data = $this->get_product_data( $product ); + protected function get_downloads( $product ) { + $downloads = array(); - // Add variations to variable products. - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - $data['variations'] = $product->get_children(); + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } } - // Add grouped products data. - if ( $product->is_type( 'grouped' ) && $product->has_child() ) { - $data['grouped_products'] = $product->get_children(); + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); } - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); + return $terms; + } - // Wrap the data in a response object. - $response = rest_ensure_response( $data ); + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); - $response->add_links( $this->prepare_links( $product, $request ) ); + // Add featured image. + if ( has_post_thumbnail( $product->get_id() ) ) { + $attachment_ids[] = $product->get_image_id(); + } - /** - * 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 ); + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_label( $key ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => str_replace( 'pa_', '', $key ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + // Variation attributes. + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( ! $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_label( $name ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $name, + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + if ( $attribute['is_taxonomy'] ) { + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), + 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $attribute['name'], + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + } + + return $attributes; } /** @@ -203,22 +529,444 @@ class WC_REST_Products_Controller extends WC_REST_Products_V1_Controller { * @return array */ protected function get_product_data( $product ) { - $data = parent::get_product_data( $product ); - $data['meta_data'] = $product->get_meta_data(); + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from() ) : '', + 'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to() ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales(), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->get_backorders(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => $product->get_shipping_class_id(), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $product->get_menu_order(), + 'meta_data' => $product->get_meta_data(), + ); return $data; } /** - * Set product meta. + * Prepare links for the request. * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * @return WC_Product + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. */ - protected function set_product_meta( $product, $request ) { - $product = parent::set_product_meta( $product, $request ); + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } // Allow set meta_data. if ( is_array( $request['meta_data'] ) ) { @@ -227,35 +975,341 @@ class WC_REST_Products_Controller extends WC_REST_Products_V1_Controller { } } + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + return $product; } /** - * Update post meta fields. + * Save product shipping data. * - * @param WP_Post $post Post data. - * @param WP_REST_Request $request Request data. - * @return bool|WP_Error + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * @return WC_Product */ - protected function update_post_meta_fields( $post, $request ) { - $product = wc_get_product( $post ); + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } - // Check for featured/gallery images, upload it and set it. - if ( isset( $request['images'] ) ) { - $product = $this->set_product_images( $product, $request['images'] ); + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } } - // Save product meta fields. - $product = $this->set_product_meta( $product, $request ); + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $shipping_class_term = get_term_by( 'slug', wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); - // Save the product data. - $product->save(); + if ( $shipping_class_term ) { + $product->set_shipping_class_id( $shipping_class_term->term_id ); + } + } - // Clear caches here so in sync with any new variations/children. - wc_delete_product_transients( $product->get_id() ); - wp_cache_delete( 'product-' . $product->get_id(), 'products' ); + return $product; + } - return true; + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 2.7. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '2.7', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + $term_ids = array_unique( array_map( 'intval', $term_ids ) ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @since 2.7.0 + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_variation_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $parent_id = $object->get_parent_id(); + $result = false; + + if ( 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->delete( true ); + } + } elseif ( $object->is_type( 'grouped' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + $child->set_parent_id( 0 ); + $child->save(); + } + } + + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( 0 !== $parent_id ) { + wc_delete_product_transients( $parent_id ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; } /** From 55977d03f398f6a4d520f282c2c717dacd9c7375 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 22:40:37 -0200 Subject: [PATCH 09/16] Do not allow manipulate variations on products endpoint --- includes/api/class-wc-rest-products-controller.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 803680b77ed..f9624cf7bb9 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -652,6 +652,10 @@ class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { $product = new WC_Product_Simple(); } + if ( 'variation' === $product->get_type() ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } + // Post title. if ( isset( $request['name'] ) ) { $product->set_name( wp_filter_post_kses( $request['name'] ) ); @@ -1234,6 +1238,10 @@ class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } + if ( 'variation' === $object->get_type() ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); /** From 94b0800735b1e4033745831f2e5be683648335ce Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Thu, 16 Feb 2017 22:51:36 -0200 Subject: [PATCH 10/16] Use CRUD Controller for product variations --- ...-wc-rest-product-variations-controller.php | 270 ++++++++---------- 1 file changed, 124 insertions(+), 146 deletions(-) diff --git a/includes/api/class-wc-rest-product-variations-controller.php b/includes/api/class-wc-rest-product-variations-controller.php index ea3bc534a7b..5efbdaff04d 100644 --- a/includes/api/class-wc-rest-product-variations-controller.php +++ b/includes/api/class-wc-rest-product-variations-controller.php @@ -133,96 +133,111 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller } /** - * Adds the parent product ID to the query so we filter / get the correct variations. + * Get object. * - * @param array $args - * @param WP_REST_Request $request - * @return array + * @since 2.7.0 + * @param int $id Object ID. + * @return WC_Data */ - public function add_product_id( $args, $request ) { - $args['post_parent'] = $request['product_id']; - return $args; + protected function get_object( $id ) { + return wc_get_product( $id ); } /** * Prepare a single variation output for response. * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. + * @since 2.7.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ - public function prepare_item_for_response( $post, $request ) { - $variation = wc_get_product( $post ); - $data = array( - 'id' => $variation->get_id(), - 'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ), - 'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ), - 'description' => $variation->get_description(), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price(), - 'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from() ) : '', - 'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to() ) : '', - 'on_sale' => $variation->is_on_sale(), - 'visible' => $variation->is_visible(), - 'purchasable' => $variation->is_purchasable(), - 'virtual' => $variation->is_virtual(), - 'downloadable' => $variation->is_downloadable(), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1, - 'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1, - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'manage_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders' => $variation->get_backorders(), - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'weight' => $variation->get_weight(), + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => $object->get_description(), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => $object->get_date_on_sale_from() ? date( 'Y-m-d', $object->get_date_on_sale_from() ) : '', + 'date_on_sale_to' => $object->get_date_on_sale_to() ? date( 'Y-m-d', $object->get_date_on_sale_to() ) : '', + 'on_sale' => $object->is_on_sale(), + 'visible' => $object->is_visible(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'in_stock' => $object->is_in_stock(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'weight' => $object->get_weight(), 'dimensions' => array( - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->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 ), - 'menu_order' => $variation->get_menu_order(), - 'meta_data' => $variation->get_meta_data(), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => $this->get_images( $object ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), ); - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - // Wrap the data in a response object. + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $variation, $request ) ); + $response->add_links( $this->prepare_links( $object, $request ) ); /** * Filter the data for a response. * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + + + /** + * Prepare objects query. + * + * @since 2.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + $args['post_parent'] = $request['product_id']; + + return $args; } /** * Prepare a single variation for create or update. * - * @param WP_REST_Request $request Request object. - * @return WP_Error|stdClass Post object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data */ - protected function prepare_item_for_database( $request ) { + protected function prepare_object_for_database( $request, $creating = false ) { if ( isset( $request['id'] ) ) { $variation = wc_get_product( absint( $request['id'] ) ); } else { @@ -231,62 +246,6 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller $variation->set_parent_id( absint( $request['product_id'] ) ); - /** - * Filter the query_vars used in `get_items` for the constructed query. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for insertion. - * - * @param WC_Product_Variation $variation 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}", $variation, $request ); - } - - /** - * Add post meta fields. - * - * @param WP_Post $post - * @param WP_REST_Request $request Request data. - * @return bool - */ - protected function add_post_meta_fields( $post, $request ) { - return $this->update_post_meta_fields( $post, $request ); - } - - /** - * Update post meta fields. - * - * @param WP_Post $post - * @param WP_REST_Request $request Request data. - * @return bool - */ - protected function update_post_meta_fields( $post, $request ) { - $variation = wc_get_product( $post ); - - // Save variation meta fields. - $variation = $this->set_variation_meta( $variation, $request ); - - // Save the variation data. - $variation->save(); - - // Clear caches here so in sync with any new variations. - wc_delete_product_transients( $variation->get_parent_id() ); - wp_cache_delete( 'product-' . $variation->get_parent_id(), 'products' ); - - return true; - } - - /** - * Set variation meta. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * @return WC_Product_Variation - */ - protected function set_variation_meta( $variation, $request ) { // Status. if ( isset( $request['visible'] ) ) { $variation->set_status( false === $request['visible'] ? 'private' : 'publish' ); @@ -451,7 +410,27 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller } } - return $variation; + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Clear caches here so in sync with any new variations. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_parent_id() ); + wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' ); } /** @@ -494,6 +473,30 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller return parent::batch_items( $request ); } + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + return $links; + } + /** * Get the Variation's schema, conforming to JSON Schema. * @@ -832,29 +835,4 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller return $this->add_additional_fields_schema( $schema ); } - - /** - * Prepare links for the request. - * - * @param WC_Product $product Product object. - * @param WP_REST_Request $request Request object. - * @return array Links for the given product. - */ - protected function prepare_links( $variation, $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, $variation->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), - ), - 'up' => array( - 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), - ), - ); - return $links; - } - } From 18dc63757b5278efddf627035214435548ebd07e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 00:09:46 -0200 Subject: [PATCH 11/16] BCheck if object exists --- .../abstract-wc-rest-crud-controller.php | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-crud-controller.php b/includes/abstracts/abstract-wc-rest-crud-controller.php index 4cf52f0a300..f271ea95275 100644 --- a/includes/abstracts/abstract-wc-rest-crud-controller.php +++ b/includes/abstracts/abstract-wc-rest-crud-controller.php @@ -33,6 +33,16 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { */ protected $hierarchical = false; + /** + * Get object. + * + * @param int $id Object ID. + * @return WP_Error|WC_Data + */ + protected function get_object( $id ) { + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + /** * Check if a given request has access to read an item. * @@ -40,7 +50,9 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { - if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', (int) $request['id'] ) ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -54,7 +66,9 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { - if ( ! wc_rest_check_post_permissions( $this->post_type, 'edit', (int) $request['id'] ) ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -68,23 +82,15 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { - if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', (int) $request['id'] ) ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); } return true; } - /** - * Get object. - * - * @param int $id Object ID. - * @return WP_Error|WC_Data - */ - protected function get_object( $id ) { - return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); - } - /** * Get object permalink. * @@ -128,7 +134,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { public function get_item( $request ) { $object = $this->get_object( (int) $request['id'] ); - if ( 0 === $object->get_id() ) { + if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } @@ -215,7 +221,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { public function update_item( $request ) { $object = $this->get_object( (int) $request['id'] ); - if ( 0 === $object->get_id() ) { + if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } @@ -375,7 +381,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { $object = $this->get_object( (int) $request['id'] ); $result = false; - if ( 0 === $object->get_id() ) { + if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } From ba7607fe00cc6c91180bfbb7f152a4f8001f1cd3 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 00:10:25 -0200 Subject: [PATCH 12/16] Improved delete_item() for products and variations --- ...-wc-rest-product-variations-controller.php | 74 ++++++++++++++++++- .../api/class-wc-rest-products-controller.php | 15 ++-- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/includes/api/class-wc-rest-product-variations-controller.php b/includes/api/class-wc-rest-product-variations-controller.php index 5efbdaff04d..84bc1a7af7f 100644 --- a/includes/api/class-wc-rest-product-variations-controller.php +++ b/includes/api/class-wc-rest-product-variations-controller.php @@ -440,8 +440,78 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Products_Controller * @return WP_Error|boolean */ public function delete_item( $request ) { - $request['id'] = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); - return parent::delete_item( $request ); + $id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; } /** diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index f9624cf7bb9..4d39cc5321e 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -1228,13 +1228,12 @@ class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { - $id = (int) $request['id']; - $force = (bool) $request['force']; - $object = $this->get_object( (int) $request['id'] ); - $parent_id = $object->get_parent_id(); - $result = false; + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; - if ( 0 === $object->get_id() ) { + if ( ! $object || 0 === $object->get_id() ) { return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); } @@ -1304,8 +1303,8 @@ class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { } // Delete parent product transients. - if ( 0 !== $parent_id ) { - wc_delete_product_transients( $parent_id ); + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); } /** From 6af3b2255e46177212bc92be2241bca4aea06d0e Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 00:10:46 -0200 Subject: [PATCH 13/16] Fixed default used_by --- includes/class-wc-coupon.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 6c6758cd1bf..459cba03494 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -45,7 +45,7 @@ class WC_Coupon extends WC_Legacy_Coupon { 'minimum_amount' => '', 'maximum_amount' => '', 'email_restrictions' => array(), - 'used_by' => '', + 'used_by' => array(), ); // Coupon message codes From 58d90a2ee874899cef063eb0ab596465cac25a67 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 00:27:40 -0200 Subject: [PATCH 14/16] Added legacy classes --- .../api/class-wc-rest-coupons-controller.php | 2 +- .../api/class-wc-rest-orders-controller.php | 13 +- .../api/class-wc-rest-products-controller.php | 2 +- ...lass-wc-rest-legacy-coupons-controller.php | 164 ++++ ...class-wc-rest-legacy-orders-controller.php | 300 +++++++ ...ass-wc-rest-legacy-products-controller.php | 809 ++++++++++++++++++ .../v1/class-wc-rest-coupons-controller.php | 2 +- includes/class-wc-api.php | 5 + 8 files changed, 1282 insertions(+), 15 deletions(-) create mode 100644 includes/api/legacy/class-wc-rest-legacy-coupons-controller.php create mode 100644 includes/api/legacy/class-wc-rest-legacy-orders-controller.php create mode 100644 includes/api/legacy/class-wc-rest-legacy-products-controller.php diff --git a/includes/api/class-wc-rest-coupons-controller.php b/includes/api/class-wc-rest-coupons-controller.php index f36d771b9b2..e81a0009c74 100644 --- a/includes/api/class-wc-rest-coupons-controller.php +++ b/includes/api/class-wc-rest-coupons-controller.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @package WooCommerce/API * @extends WC_REST_CRUD_Controller */ -class WC_REST_Coupons_Controller extends WC_REST_CRUD_Controller { +class WC_REST_Coupons_Controller extends WC_REST_Legacy_Coupons_Controller { /** * Endpoint namespace. diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index ccf5ed876e4..896b0c585ff 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @package WooCommerce/API * @extends WC_REST_CRUD_Controller */ -class WC_REST_Orders_Controller extends WC_REST_CRUD_Controller { +class WC_REST_Orders_Controller extends WC_REST_Legacy_Orders_Controller { /** * Endpoint namespace. @@ -415,17 +415,6 @@ class WC_REST_Orders_Controller extends WC_REST_CRUD_Controller { return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); } - /** - * Create base WC Order object. - * - * @deprecated 2.7.0 - * @param array $data - * @return WC_Order - */ - protected function create_base_order( $data ) { - return wc_create_order( $data ); - } - /** * Save an object data. * diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 4d39cc5321e..153ea9eede7 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @package WooCommerce/API * @extends WC_REST_CRUD_Controller */ -class WC_REST_Products_Controller extends WC_REST_CRUD_Controller { +class WC_REST_Products_Controller extends WC_REST_Legacy_Products_Controller { /** * Endpoint namespace. diff --git a/includes/api/legacy/class-wc-rest-legacy-coupons-controller.php b/includes/api/legacy/class-wc-rest-legacy-coupons-controller.php new file mode 100644 index 00000000000..abcdd492558 --- /dev/null +++ b/includes/api/legacy/class-wc-rest-legacy-coupons-controller.php @@ -0,0 +1,164 @@ +ID ); + $data = $coupon->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : null; + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare a single coupon for create or update. + * + * @deprecated 2.7.0 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && 0 === $coupon->get_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 ) ); + } + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code' : + $coupon_code = apply_filters( 'woocommerce_coupon_code', $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description' : + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default : + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Coupon $coupon The coupon object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); + } +} diff --git a/includes/api/legacy/class-wc-rest-legacy-orders-controller.php b/includes/api/legacy/class-wc-rest-legacy-orders-controller.php new file mode 100644 index 00000000000..a5ba7968c29 --- /dev/null +++ b/includes/api/legacy/class-wc-rest-legacy-orders-controller.php @@ -0,0 +1,300 @@ + '_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 output for response. + * + * @deprecated 2.7 + * + * @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 ) { + $this->request = $request; + $statuses = wc_get_order_statuses(); + $order = wc_get_order( $post ); + $data = array_merge( array( 'id' => $order->get_id() ), $order->get_data() ); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $data[ $key ] = $data[ $key ] ? wc_rest_prepare_date_response( get_gmt_from_date( date( 'Y-m-d H:i:s', $data[ $key ] ) ) ) : false; + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + // Refunds. + $data['refunds'] = array(); + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['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 ); + $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 a single order for create. + * + * @deprecated 2.7 + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data' : + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The prder object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * + * @deprecated 2.7.0 + * + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Create order. + * + * @deprecated 2.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @deprecated 2.7.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'], $request['shipping'], $request['line_items'], $request['shipping_lines'], $request['fee_lines'], $request['coupon_lines'] ) ) { + $order->calculate_totals(); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/api/legacy/class-wc-rest-legacy-products-controller.php b/includes/api/legacy/class-wc-rest-legacy-products-controller.php new file mode 100644 index 00000000000..8e718285be6 --- /dev/null +++ b/includes/api/legacy/class-wc-rest-legacy-products-controller.php @@ -0,0 +1,809 @@ + 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $args[ $on_sale_key ] += wc_get_product_ids_on_sale(); + } + + // Apply all WP_Query filters again. + if ( is_array( $request['filter'] ) ) { + $args = array_merge( $args, $request['filter'] ); + unset( $args['filter'] ); + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Prepare a single product output for response. + * + * @deprecated 2.7.0 + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $product = wc_get_product( $post ); + $data = $this->get_product_data( $product ); + + // Add variations to variable products. + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $data['variations'] = $product->get_children(); + } + + // 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 ); + } + + /** + * Get product menu order. + * + * @deprecated 2.7.0 + * @param WC_Product $product Product instance. + * @return int + */ + protected function get_product_menu_order( $product ) { + return $product->get_menu_order(); + } + + /** + * Save product meta. + * + * @deprecated 2.7.0 + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + $product = $this->set_product_meta( $product, $request ); + $product->save(); + + return true; + } + + /** + * Set product meta. + * + * @deprecated 2.7.0 + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function set_product_meta( $product, $request ) { + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + return $product; + } + + /** + * Save variations. + * + * @deprecated 2.7.0 + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return bool + */ + protected function save_variations_data( $product, $request ) { + foreach ( $request['variations'] as $menu_order => $data ) { + $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = $data['image']; + $image = current( $image ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $variation->set_downloadable( $data['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + if ( isset( $data['manage_stock'] ) ) { + $variation->set_manage_stock( $data['manage_stock'] ); + } + + if ( isset( $data['in_stock'] ) ) { + $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $data['backorders'] ) ) { + $variation->set_backorders( $data['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + $parent_attributes = $product->get_attributes(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @deprecated 2.7.0 + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return $this->update_post_meta_fields( $post, $request ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $product = $this->set_product_meta( $product, $request ); + + // Save the product data. + $product->save(); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + } + + // Clear caches here so in sync with any new variations/children. + wc_delete_product_transients( $product->get_id() ); + wp_cache_delete( 'product-' . $product->get_id(), 'products' ); + + return true; + } + + /** + * Delete post. + * + * @deprecated 2.7.0 + * + * @param int|WP_Post $id Post ID or WP_Post instance. + */ + protected function delete_post( $id ) { + if ( ! empty( $id->ID ) ) { + $id = $id->ID; + } elseif ( ! is_numeric( $id ) || 0 >= $id ) { + return; + } + + // Delete product attachments. + $attachments = get_posts( array( + 'post_parent' => $id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + $product = wc_get_product( $id ); + $product->delete( true ); + } + + /** + * Get post types. + * + * @deprecated 2.7.0 + * + * @return array + */ + protected function get_post_types() { + return array( 'product', 'product_variation' ); + } + + /** + * Save product images. + * + * @deprecated 2.7.0 + * + * @param int $product_id + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product_id, $images ) { + $product = wc_get_product( $product_id ); + + return set_product_images( $product, $images ); + } +} diff --git a/includes/api/v1/class-wc-rest-coupons-controller.php b/includes/api/v1/class-wc-rest-coupons-controller.php index 2a26cc4f7ba..826af6a0de0 100644 --- a/includes/api/v1/class-wc-rest-coupons-controller.php +++ b/includes/api/v1/class-wc-rest-coupons-controller.php @@ -44,7 +44,7 @@ class WC_REST_Coupons_V1_Controller extends WC_REST_Posts_Controller { protected $post_type = 'shop_coupon'; /** - * Order refunds actions. + * Coupons actions. */ public function __construct() { add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); diff --git a/includes/class-wc-api.php b/includes/class-wc-api.php index fc5ec890855..cdf3846687a 100644 --- a/includes/class-wc-api.php +++ b/includes/class-wc-api.php @@ -165,6 +165,11 @@ class WC_API extends WC_Legacy_API { include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhook-deliveries.php' ); include_once( dirname( __FILE__ ) . '/api/v1/class-wc-rest-webhooks-controller.php' ); + // Legacy v2 code. + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-coupons-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-orders-controller.php' ); + include_once( dirname( __FILE__ ) . '/api/legacy/class-wc-rest-legacy-products-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' ); From a46aea1b773ac3a2532e1e898734a2e146a59ffb Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 01:17:52 -0200 Subject: [PATCH 15/16] Fixedd status --- includes/abstracts/abstract-wc-rest-crud-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/abstracts/abstract-wc-rest-crud-controller.php b/includes/abstracts/abstract-wc-rest-crud-controller.php index f271ea95275..d2d84de8db9 100644 --- a/includes/abstracts/abstract-wc-rest-crud-controller.php +++ b/includes/abstracts/abstract-wc-rest-crud-controller.php @@ -222,7 +222,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { $object = $this->get_object( (int) $request['id'] ); if ( ! $object || 0 === $object->get_id() ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); } $object = $this->save_object( $request, false ); From b0e6a5ab5de8cc8f5985118b0c8aef500dc41522 Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 17 Feb 2017 01:46:42 -0200 Subject: [PATCH 16/16] Get object again after saved --- includes/abstracts/abstract-wc-rest-crud-controller.php | 2 +- includes/api/class-wc-rest-order-refunds-controller.php | 2 +- includes/api/class-wc-rest-orders-controller.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-crud-controller.php b/includes/abstracts/abstract-wc-rest-crud-controller.php index d2d84de8db9..d7747a662f2 100644 --- a/includes/abstracts/abstract-wc-rest-crud-controller.php +++ b/includes/abstracts/abstract-wc-rest-crud-controller.php @@ -166,7 +166,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller { $object->save(); - return $object; + return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { diff --git a/includes/api/class-wc-rest-order-refunds-controller.php b/includes/api/class-wc-rest-order-refunds-controller.php index f8e7947e261..c17739e3845 100644 --- a/includes/api/class-wc-rest-order-refunds-controller.php +++ b/includes/api/class-wc-rest-order-refunds-controller.php @@ -305,7 +305,7 @@ class WC_REST_Order_Refunds_Controller extends WC_REST_Orders_Controller { return $object; } - return $object; + return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) { diff --git a/includes/api/class-wc-rest-orders-controller.php b/includes/api/class-wc-rest-orders-controller.php index 896b0c585ff..091a8ad491b 100644 --- a/includes/api/class-wc-rest-orders-controller.php +++ b/includes/api/class-wc-rest-orders-controller.php @@ -461,7 +461,7 @@ class WC_REST_Orders_Controller extends WC_REST_Legacy_Orders_Controller { } } - return $object; + return $this->get_object( $object->get_id() ); } catch ( WC_Data_Exception $e ) { return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); } catch ( WC_REST_Exception $e ) {