diff --git a/src/RestApi/Version4/Controllers/Customers.php b/src/RestApi/Version4/Controllers/Customers.php index 762fd18e72d..221f64c2069 100644 --- a/src/RestApi/Version4/Controllers/Customers.php +++ b/src/RestApi/Version4/Controllers/Customers.php @@ -14,7 +14,7 @@ defined( 'ABSPATH' ) || exit; use \WC_REST_Controller; /** - * REST API Coupons controller class. + * REST API Customers controller class. */ class Customers extends WC_REST_Controller { diff --git a/src/RestApi/Version4/Controllers/Orders.php b/src/RestApi/Version4/Controllers/Orders.php index 21504eb461c..96920472531 100644 --- a/src/RestApi/Version4/Controllers/Orders.php +++ b/src/RestApi/Version4/Controllers/Orders.php @@ -14,7 +14,7 @@ defined( 'ABSPATH' ) || exit; use \WC_REST_CRUD_Controller; /** - * REST API Coupons controller class. + * REST API Orders controller class. */ class Orders extends WC_REST_CRUD_Controller { diff --git a/src/RestApi/Version4/class-wc-rest-products-v1-controller.php b/src/RestApi/Version4/Controllers/Products.php similarity index 52% rename from src/RestApi/Version4/class-wc-rest-products-v1-controller.php rename to src/RestApi/Version4/Controllers/Products.php index 6ca69c397c0..d60b523df75 100644 --- a/src/RestApi/Version4/class-wc-rest-products-v1-controller.php +++ b/src/RestApi/Version4/Controllers/Products.php @@ -4,30 +4,26 @@ * * Handles requests to the /products endpoint. * - * @author WooThemes - * @category API * @package WooCommerce/RestApi - * @since 3.0.0 */ -if ( ! defined( 'ABSPATH' ) ) { - exit; -} +namespace WooCommerce\RestApi\Version4\Controllers; + +defined( 'ABSPATH' ) || exit; + +use \WC_REST_CRUD_Controller; /** * REST API Products controller class. - * - * @package WooCommerce/RestApi - * @extends WC_REST_Posts_Controller */ -class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { +class Products extends WC_REST_CRUD_Controller { /** * Endpoint namespace. * * @var string */ - protected $namespace = 'wc/v1'; + protected $namespace = 'wc/v4'; /** * Route base. @@ -43,626 +39,123 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { */ protected $post_type = 'product'; + /** + * If object is hierarchical. + * + * @var bool + */ + protected $hierarchical = true; + /** * Initialize product actions. */ public function __construct() { - add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); - add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); + 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( + register_rest_route( + $this->namespace, + '/' . $this->rest_base, 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' ), - ) ); + 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', - ), - ), + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', 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', + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', ), ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) ); - - register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'batch_items' ), - 'permission_callback' => array( $this, 'batch_items_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_batch_schema' ), - ) ); - } - - /** - * Get post types. - * - * @return array - */ - protected function get_post_types() { - return array( 'product', 'product_variation' ); - } - - /** - * Query args. - * - * @param array $args Request args. - * @param WP_REST_Request $request Request data. - * @return array - */ - public function query_args( $args, $request ) { - // Set post_status. - $args['post_status'] = $request['status']; - - // Taxonomy query to filter products by type, category, - // tag, shipping class, and attribute. - $tax_query = array(); - - // Map between taxonomy name and arg's key. - $taxonomies = array( - 'product_cat' => 'category', - 'product_tag' => 'tag', - 'product_shipping_class' => 'shipping_class', - ); - - // Set tax_query for each passed arg. - foreach ( $taxonomies as $taxonomy => $key ) { - if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) { - $request[ $key ] = array_filter( $request[ $key ] ); - } - - if ( ! empty( $request[ $key ] ) ) { - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'term_id', - 'terms' => $request[ $key ], - ); - } - } - - // Filter product type by slug. - if ( ! empty( $request['type'] ) ) { - $tax_query[] = array( - 'taxonomy' => 'product_type', - 'field' => 'slug', - 'terms' => $request['type'], - ); - } - - // Filter by attribute and term. - if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { - if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { - $tax_query[] = array( - 'taxonomy' => $request['attribute'], - 'field' => 'term_id', - 'terms' => $request['attribute_term'], - ); - } - } - - if ( ! empty( $tax_query ) ) { - $args['tax_query'] = $tax_query; - } - - // Filter by sku. - if ( ! empty( $request['sku'] ) ) { - $skus = explode( ',', $request['sku'] ); - // Include the current string as a SKU too. - if ( 1 < count( $skus ) ) { - $skus[] = $request['sku']; - } - - $args['meta_query'] = $this->add_meta_query( $args, array( - 'key' => '_sku', - 'value' => $skus, - 'compare' => 'IN', - ) ); - } - - // Apply all WP_Query filters again. - if ( is_array( $request['filter'] ) ) { - $args = array_merge( $args, $request['filter'] ); - unset( $args['filter'] ); - } - - // Force the post_type argument, since it's not a user input variable. - if ( ! empty( $request['sku'] ) ) { - $args['post_type'] = array( 'product', 'product_variation' ); - } else { - $args['post_type'] = $this->post_type; - } - - return $args; - } - - /** - * Get the downloads for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * @return array - */ - protected function get_downloads( $product ) { - $downloads = array(); - - if ( $product->is_downloadable() ) { - foreach ( $product->get_downloads() as $file_id => $file ) { - $downloads[] = array( - 'id' => $file_id, // MD5 hash. - 'name' => $file['name'], - 'file' => $file['file'], - ); - } - } - - return $downloads; - } - - /** - * Get taxonomy terms. - * - * @param WC_Product $product Product instance. - * @param string $taxonomy Taxonomy slug. - * @return array - */ - protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { - $terms = array(); - - foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { - $terms[] = array( - 'id' => $term->term_id, - 'name' => $term->name, - 'slug' => $term->slug, - ); - } - - return $terms; - } - - /** - * Get the images for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * @return array - */ - protected function get_images( $product ) { - $images = array(); - $attachment_ids = array(); - - // Add featured image. - if ( $product->get_image_id() ) { - $attachment_ids[] = $product->get_image_id(); - } - - // Add gallery images. - $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); - - // Build image data. - foreach ( $attachment_ids as $position => $attachment_id ) { - $attachment_post = get_post( $attachment_id ); - if ( is_null( $attachment_post ) ) { - continue; - } - - $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); - if ( ! is_array( $attachment ) ) { - continue; - } - - $images[] = array( - 'id' => (int) $attachment_id, - 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), - 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), - 'src' => current( $attachment ), - 'name' => get_the_title( $attachment_id ), - 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), - 'position' => (int) $position, - ); - } - - // Set a placeholder image if the product has no images set. - if ( empty( $images ) ) { - $images[] = array( - 'id' => 0, - 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. - 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), - 'src' => wc_placeholder_img_src(), - 'name' => __( 'Placeholder', 'woocommerce' ), - 'alt' => __( 'Placeholder', 'woocommerce' ), - 'position' => 0, - ); - } - - return $images; - } - - /** - * Get attribute taxonomy label. - * - * @param string $name Taxonomy name. - * @return string - */ - protected function get_attribute_taxonomy_label( $name ) { - $tax = get_taxonomy( $name ); - $labels = get_taxonomy_labels( $tax ); - - return $labels->singular_name; - } - - /** - * Get default attributes. - * - * @param WC_Product $product Product instance. - * @return array - */ - protected function get_default_attributes( $product ) { - $default = array(); - - if ( $product->is_type( 'variable' ) ) { - foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { - if ( 0 === strpos( $key, 'pa_' ) ) { - $default[] = array( - 'id' => wc_attribute_taxonomy_id_by_name( $key ), - 'name' => $this->get_attribute_taxonomy_label( $key ), - 'option' => $value, - ); - } else { - $default[] = array( - 'id' => 0, - 'name' => wc_attribute_taxonomy_slug( $key ), - 'option' => $value, - ); - } - } - } - - return $default; - } - - /** - * Get attribute options. - * - * @param int $product_id Product ID. - * @param array $attribute Attribute data. - * @return array - */ - protected function get_attribute_options( $product_id, $attribute ) { - if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { - return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); - } elseif ( isset( $attribute['value'] ) ) { - return array_map( 'trim', explode( '|', $attribute['value'] ) ); - } - - return array(); - } - - /** - * Get the attributes for a product or product variation. - * - * @param WC_Product|WC_Product_Variation $product Product instance. - * @return array - */ - protected function get_attributes( $product ) { - $attributes = array(); - - if ( $product->is_type( 'variation' ) ) { - // Variation attributes. - foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { - $name = str_replace( 'attribute_', '', $attribute_name ); - - if ( ! $attribute ) { - continue; - } - - // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. - if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { - $option_term = get_term_by( 'slug', $attribute, $name ); - $attributes[] = array( - 'id' => wc_attribute_taxonomy_id_by_name( $name ), - 'name' => $this->get_attribute_taxonomy_label( $name ), - 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, - ); - } else { - $attributes[] = array( - 'id' => 0, - 'name' => $name, - 'option' => $attribute, - ); - } - } - } else { - foreach ( $product->get_attributes() as $attribute ) { - if ( $attribute['is_taxonomy'] ) { - $attributes[] = array( - 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), - 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), - 'position' => (int) $attribute['position'], - 'visible' => (bool) $attribute['is_visible'], - 'variation' => (bool) $attribute['is_variation'], - 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), - ); - } else { - $attributes[] = array( - 'id' => 0, - 'name' => $attribute['name'], - 'position' => (int) $attribute['position'], - 'visible' => (bool) $attribute['is_visible'], - 'variation' => (bool) $attribute['is_variation'], - 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), - ); - } - } - } - - return $attributes; - } - - /** - * Get product menu order. - * - * @deprecated 3.0.0 - * @param WC_Product $product Product instance. - * @return int - */ - protected function get_product_menu_order( $product ) { - return $product->get_menu_order(); - } - - /** - * Get product data. - * - * @param WC_Product $product Product instance. - * @return array - */ - protected function get_product_data( $product ) { - $data = array( - 'id' => $product->get_id(), - 'name' => $product->get_name(), - 'slug' => $product->get_slug(), - 'permalink' => $product->get_permalink(), - 'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ), - 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ), - 'type' => $product->get_type(), - 'status' => $product->get_status(), - 'featured' => $product->is_featured(), - 'catalog_visibility' => $product->get_catalog_visibility(), - 'description' => wpautop( do_shortcode( $product->get_description() ) ), - 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), - 'sku' => $product->get_sku(), - 'price' => $product->get_price(), - 'regular_price' => $product->get_regular_price(), - 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', - 'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '', - 'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '', - 'price_html' => $product->get_price_html(), - 'on_sale' => $product->is_on_sale(), - 'purchasable' => $product->is_purchasable(), - 'total_sales' => $product->get_total_sales(), - 'virtual' => $product->is_virtual(), - 'downloadable' => $product->is_downloadable(), - 'downloads' => $this->get_downloads( $product ), - 'download_limit' => $product->get_download_limit(), - 'download_expiry' => $product->get_download_expiry(), - 'download_type' => 'standard', - 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', - 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', - 'tax_status' => $product->get_tax_status(), - 'tax_class' => $product->get_tax_class(), - 'manage_stock' => $product->managing_stock(), - 'stock_quantity' => $product->get_stock_quantity(), - 'in_stock' => $product->is_in_stock(), - 'backorders' => $product->get_backorders(), - 'backorders_allowed' => $product->backorders_allowed(), - 'backordered' => $product->is_on_backorder(), - 'sold_individually' => $product->is_sold_individually(), - 'weight' => $product->get_weight(), - 'dimensions' => array( - 'length' => $product->get_length(), - 'width' => $product->get_width(), - 'height' => $product->get_height(), - ), - 'shipping_required' => $product->needs_shipping(), - 'shipping_taxable' => $product->is_shipping_taxable(), - 'shipping_class' => $product->get_shipping_class(), - 'shipping_class_id' => $product->get_shipping_class_id(), - 'reviews_allowed' => $product->get_reviews_allowed(), - 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), - 'rating_count' => $product->get_rating_count(), - 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), - 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), - 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), - 'parent_id' => $product->get_parent_id(), - 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), - 'categories' => $this->get_taxonomy_terms( $product ), - 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), - 'images' => $this->get_images( $product ), - 'attributes' => $this->get_attributes( $product ), - 'default_attributes' => $this->get_default_attributes( $product ), - 'variations' => array(), - 'grouped_products' => array(), - 'menu_order' => $product->get_menu_order(), - ); - - return $data; - } - - /** - * Get an individual variation's data. - * - * @param WC_Product $product Product instance. - * @return array - */ - protected function get_variation_data( $product ) { - $variations = array(); - - foreach ( $product->get_children() as $child_id ) { - $variation = wc_get_product( $child_id ); - if ( ! $variation || ! $variation->exists() ) { - continue; - } - - $variations[] = array( - 'id' => $variation->get_id(), - 'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ), - 'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ), - 'permalink' => $variation->get_permalink(), - 'sku' => $variation->get_sku(), - 'price' => $variation->get_price(), - 'regular_price' => $variation->get_regular_price(), - 'sale_price' => $variation->get_sale_price(), - 'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '', - 'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '', - 'on_sale' => $variation->is_on_sale(), - 'purchasable' => $variation->is_purchasable(), - 'visible' => $variation->is_visible(), - 'virtual' => $variation->is_virtual(), - 'downloadable' => $variation->is_downloadable(), - 'downloads' => $this->get_downloads( $variation ), - 'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1, - 'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1, - 'tax_status' => $variation->get_tax_status(), - 'tax_class' => $variation->get_tax_class(), - 'manage_stock' => $variation->managing_stock(), - 'stock_quantity' => $variation->get_stock_quantity(), - 'in_stock' => $variation->is_in_stock(), - 'backorders' => $variation->get_backorders(), - 'backorders_allowed' => $variation->backorders_allowed(), - 'backordered' => $variation->is_on_backorder(), - 'weight' => $variation->get_weight(), - 'dimensions' => array( - 'length' => $variation->get_length(), - 'width' => $variation->get_width(), - 'height' => $variation->get_height(), + 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', + ) + ), + ), ), - 'shipping_class' => $variation->get_shipping_class(), - 'shipping_class_id' => $variation->get_shipping_class_id(), - 'image' => $this->get_images( $variation ), - 'attributes' => $this->get_attributes( $variation ), - ); - } - - return $variations; - } - - /** - * Prepare a single product output for response. - * - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response - */ - public function prepare_item_for_response( $post, $request ) { - $product = wc_get_product( $post ); - $data = $this->get_product_data( $product ); - - // Add variations to variable products. - if ( $product->is_type( 'variable' ) && $product->has_child() ) { - $data['variations'] = $this->get_variation_data( $product ); - } - - // Add grouped products data. - if ( $product->is_type( 'grouped' ) && $product->has_child() ) { - $data['grouped_products'] = $product->get_children(); - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - // Wrap the data in a response object. - $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $product, $request ) ); - - /** - * Filter the data for a response. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for the response. - * - * @param WP_REST_Response $response The response object. - * @param WP_Post $post Post object. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); - } - - /** - * Prepare links for the request. - * - * @param WC_Product $product Product object. - * @param WP_REST_Request $request Request object. - * @return array Links for the given product. - */ - protected function prepare_links( $product, $request ) { - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ), - ), - 'collection' => array( - 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), - ), + 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' ), + ) ); - if ( $product->get_parent_id() ) { - $links['up'] = array( - 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ), - ); - } + 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' ), + ) + ); + } - return $links; + /** + * Get object. + * + * @param int $id Object ID. + * + * @since 3.0.0 + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); } /** * Prepare a single product 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 ) { + 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. @@ -680,6 +173,16 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_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'] ) ); @@ -715,366 +218,6 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { $product->set_reviews_allowed( $request['reviews_allowed'] ); } - /** - * Filter the query_vars used in `get_items` for the constructed query. - * - * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being - * prepared for insertion. - * - * @param WC_Product $product An object representing a single item prepared - * for inserting or updating the database. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request ); - } - - /** - * Create a single product. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); - } - - $product_id = 0; - - try { - $product_id = $this->save_product( $request ); - $post = get_post( $product_id ); - $this->update_additional_fields_for_object( $post, $request ); - $this->update_post_meta_fields( $post, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Post $post Post data. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( 'woocommerce_rest_insert_product', $post, $request, true ); - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - $response = rest_ensure_response( $response ); - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); - - return $response; - } catch ( WC_Data_Exception $e ) { - $this->delete_post( $product_id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - $this->delete_post( $product_id ); - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Update a single product. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_Error|WP_REST_Response - */ - public function update_item( $request ) { - $post_id = (int) $request['id']; - - if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); - } - - try { - $product_id = $this->save_product( $request ); - $post = get_post( $product_id ); - $this->update_additional_fields_for_object( $post, $request ); - $this->update_post_meta_fields( $post, $request ); - - /** - * Fires after a single item is created or updated via the REST API. - * - * @param WP_Post $post Post data. - * @param WP_REST_Request $request Request object. - * @param boolean $creating True when creating item, false when updating. - */ - do_action( 'woocommerce_rest_insert_product', $post, $request, false ); - $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); - - return rest_ensure_response( $response ); - } catch ( WC_Data_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); - } catch ( WC_REST_Exception $e ) { - return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); - } - } - - /** - * Saves a product to the database. - * - * @param WP_REST_Request $request Full details about the request. - * @return int - */ - public function save_product( $request ) { - $product = $this->prepare_item_for_database( $request ); - return $product->save(); - } - - /** - * Save product images. - * - * @deprecated 3.0.0 - * @param int $product_id - * @param array $images - * @throws WC_REST_Exception - */ - protected function save_product_images( $product_id, $images ) { - $product = wc_get_product( $product_id ); - - return set_product_images( $product, $images ); - } - - /** - * Set product images. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param array $images Images data. - * @return WC_Product - */ - protected function set_product_images( $product, $images ) { - if ( is_array( $images ) ) { - $gallery = array(); - - foreach ( $images as $image ) { - $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; - - if ( 0 === $attachment_id && isset( $image['src'] ) ) { - $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); - - if ( is_wp_error( $upload ) ) { - if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { - throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); - } else { - continue; - } - } - - $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); - } - - if ( ! wp_attachment_is_image( $attachment_id ) ) { - throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); - } - - if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { - $product->set_image_id( $attachment_id ); - } else { - $gallery[] = $attachment_id; - } - - // Set the image alt if present. - if ( ! empty( $image['alt'] ) ) { - update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); - } - - // Set the image name if present. - if ( ! empty( $image['name'] ) ) { - wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); - } - } - - if ( ! empty( $gallery ) ) { - $product->set_gallery_image_ids( $gallery ); - } - } else { - $product->set_image_id( '' ); - $product->set_gallery_image_ids( array() ); - } - - return $product; - } - - /** - * Save product shipping data. - * - * @param WC_Product $product Product instance. - * @param array $data Shipping data. - * @return WC_Product - */ - protected function save_product_shipping_data( $product, $data ) { - // Virtual. - if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { - $product->set_weight( '' ); - $product->set_height( '' ); - $product->set_length( '' ); - $product->set_width( '' ); - } else { - if ( isset( $data['weight'] ) ) { - $product->set_weight( $data['weight'] ); - } - - // Height. - if ( isset( $data['dimensions']['height'] ) ) { - $product->set_height( $data['dimensions']['height'] ); - } - - // Width. - if ( isset( $data['dimensions']['width'] ) ) { - $product->set_width( $data['dimensions']['width'] ); - } - - // Length. - if ( isset( $data['dimensions']['length'] ) ) { - $product->set_length( $data['dimensions']['length'] ); - } - } - - // Shipping class. - if ( isset( $data['shipping_class'] ) ) { - $data_store = $product->get_data_store(); - $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); - $product->set_shipping_class_id( $shipping_class_id ); - } - - return $product; - } - - /** - * Save downloadable files. - * - * @param WC_Product $product Product instance. - * @param array $downloads Downloads data. - * @param int $deprecated Deprecated since 3.0. - * @return WC_Product - */ - protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { - if ( $deprecated ) { - wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); - } - - $files = array(); - foreach ( $downloads as $key => $file ) { - if ( empty( $file['file'] ) ) { - continue; - } - - $download = new WC_Product_Download(); - $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); - $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); - $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); - $files[] = $download; - } - $product->set_downloads( $files ); - - return $product; - } - - /** - * Save taxonomy terms. - * - * @param WC_Product $product Product instance. - * @param array $terms Terms data. - * @param string $taxonomy Taxonomy name. - * @return WC_Product - */ - protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { - $term_ids = wp_list_pluck( $terms, 'id' ); - - if ( 'cat' === $taxonomy ) { - $product->set_category_ids( $term_ids ); - } elseif ( 'tag' === $taxonomy ) { - $product->set_tag_ids( $term_ids ); - } - - return $product; - } - - /** - * Save default attributes. - * - * @since 3.0.0 - * - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * @return WC_Product - */ - protected function save_default_attributes( $product, $request ) { - if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { - $attributes = $product->get_attributes(); - $default_attributes = array(); - - foreach ( $request['default_attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = sanitize_title( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( isset( $attributes[ $attribute_name ] ) ) { - $_attribute = $attributes[ $attribute_name ]; - - if ( $_attribute['is_variation'] ) { - $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - - if ( ! empty( $_attribute['is_taxonomy'] ) ) { - // If dealing with a taxonomy, we need to get the slug from the name posted to the API. - $term = get_term_by( 'name', $value, $attribute_name ); - - if ( $term && ! is_wp_error( $term ) ) { - $value = $term->slug; - } else { - $value = sanitize_title( $value ); - } - } - - if ( $value ) { - $default_attributes[ $attribute_name ] = $value; - } - } - } - } - - $product->set_default_attributes( $default_attributes ); - } - - return $product; - } - - /** - * Save product meta. - * - * @deprecated 3.0.0 - * @param WC_Product $product - * @param WP_REST_Request $request - * @return bool - * @throws WC_REST_Exception - */ - protected function save_product_meta( $product, $request ) { - $product = $this->set_product_meta( $product, $request ); - $product->save(); - - return true; - } - - /** - * Set product meta. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * @return WC_Product - */ - protected function set_product_meta( $product, $request ) { // Virtual. if ( isset( $request['virtual'] ) ) { $product->set_virtual( $request['virtual'] ); @@ -1201,12 +344,20 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { $product->set_date_on_sale_from( $request['date_on_sale_from'] ); } + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + if ( isset( $request['date_on_sale_to'] ) ) { $product->set_date_on_sale_to( $request['date_on_sale_to'] ); } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } } - // Product parent ID for groups. + // Product parent ID. if ( isset( $request['parent_id'] ) ) { $product->set_parent_id( $request['parent_id'] ); } @@ -1216,9 +367,9 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { $product->set_sold_individually( $request['sold_individually'] ); } - // Stock status. - if ( isset( $request['in_stock'] ) ) { - $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + // Stock status; stock_status has priority over in_stock. + if ( isset( $request['stock_status'] ) ) { + $stock_status = $request['stock_status']; } else { $stock_status = $product->get_stock_status(); } @@ -1351,318 +502,967 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { $product = $this->save_default_attributes( $product, $request ); } - return $product; - } - - /** - * Save variations. - * - * @throws WC_REST_Exception REST API exceptions. - * @param WC_Product $product Product instance. - * @param WP_REST_Request $request Request data. - * @return bool - */ - protected function save_variations_data( $product, $request ) { - foreach ( $request['variations'] as $menu_order => $data ) { - $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); - - // Create initial name and status. - if ( ! $variation->get_slug() ) { - /* translators: 1: variation id 2: product name */ - $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); - $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); - } - - // Parent ID. - $variation->set_parent_id( $product->get_id() ); - - // Menu order. - $variation->set_menu_order( $menu_order ); - - // Status. - if ( isset( $data['visible'] ) ) { - $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); - } - - // SKU. - if ( isset( $data['sku'] ) ) { - $variation->set_sku( wc_clean( $data['sku'] ) ); - } - - // Thumbnail. - if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { - $image = $data['image']; - $image = current( $image ); - if ( is_array( $image ) ) { - $image['position'] = 0; - } - - $variation = $this->set_product_images( $variation, array( $image ) ); - } - - // Virtual variation. - if ( isset( $data['virtual'] ) ) { - $variation->set_virtual( $data['virtual'] ); - } - - // Downloadable variation. - if ( isset( $data['downloadable'] ) ) { - $variation->set_downloadable( $data['downloadable'] ); - } - - // Downloads. - if ( $variation->get_downloadable() ) { - // Downloadable files. - if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { - $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); - } - - // Download limit. - if ( isset( $data['download_limit'] ) ) { - $variation->set_download_limit( $data['download_limit'] ); - } - - // Download expiry. - if ( isset( $data['download_expiry'] ) ) { - $variation->set_download_expiry( $data['download_expiry'] ); - } - } - - // Shipping data. - $variation = $this->save_product_shipping_data( $variation, $data ); - - // Stock handling. - if ( isset( $data['manage_stock'] ) ) { - $variation->set_manage_stock( $data['manage_stock'] ); - } - - if ( isset( $data['in_stock'] ) ) { - $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); - } - - if ( isset( $data['backorders'] ) ) { - $variation->set_backorders( $data['backorders'] ); - } - - if ( $variation->get_manage_stock() ) { - if ( isset( $data['stock_quantity'] ) ) { - $variation->set_stock_quantity( $data['stock_quantity'] ); - } elseif ( isset( $data['inventory_delta'] ) ) { - $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); - $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); - $variation->set_stock_quantity( $stock_quantity ); - } - } else { - $variation->set_backorders( 'no' ); - $variation->set_stock_quantity( '' ); - } - - // Regular Price. - if ( isset( $data['regular_price'] ) ) { - $variation->set_regular_price( $data['regular_price'] ); - } - - // Sale Price. - if ( isset( $data['sale_price'] ) ) { - $variation->set_sale_price( $data['sale_price'] ); - } - - if ( isset( $data['date_on_sale_from'] ) ) { - $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); - } - - if ( isset( $data['date_on_sale_to'] ) ) { - $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); - } - - // Tax class. - if ( isset( $data['tax_class'] ) ) { - $variation->set_tax_class( $data['tax_class'] ); - } - - // Description. - if ( isset( $data['description'] ) ) { - $variation->set_description( wp_kses_post( $data['description'] ) ); - } - - // Update taxonomies. - if ( isset( $data['attributes'] ) ) { - $attributes = array(); - $parent_attributes = $product->get_attributes(); - - foreach ( $data['attributes'] as $attribute ) { - $attribute_id = 0; - $attribute_name = ''; - - // Check ID for global attributes or name for product attributes. - if ( ! empty( $attribute['id'] ) ) { - $attribute_id = absint( $attribute['id'] ); - $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); - } elseif ( ! empty( $attribute['name'] ) ) { - $attribute_name = sanitize_title( $attribute['name'] ); - } - - if ( ! $attribute_id && ! $attribute_name ) { - continue; - } - - if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { - continue; - } - - $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); - $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; - - if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { - // If dealing with a taxonomy, we need to get the slug from the name posted to the API. - $term = get_term_by( 'name', $attribute_value, $attribute_name ); - - if ( $term && ! is_wp_error( $term ) ) { - $attribute_value = $term->slug; - } else { - $attribute_value = sanitize_title( $attribute_value ); - } - } - - $attributes[ $attribute_key ] = $attribute_value; - } - - $variation->set_attributes( $attributes ); - } - - $variation->save(); - - do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); } - return true; - } - - /** - * Add post meta fields. - * - * @param WP_Post $post Post data. - * @param WP_REST_Request $request Request data. - * @return bool|WP_Error - */ - protected function add_post_meta_fields( $post, $request ) { - return $this->update_post_meta_fields( $post, $request ); - } - - /** - * Update post meta fields. - * - * @param WP_Post $post Post data. - * @param WP_REST_Request $request Request data. - * @return bool|WP_Error - */ - protected function update_post_meta_fields( $post, $request ) { - $product = wc_get_product( $post ); - // Check for featured/gallery images, upload it and set it. if ( isset( $request['images'] ) ) { $product = $this->set_product_images( $product, $request['images'] ); } - // Save product meta fields. - $product = $this->set_product_meta( $product, $request ); - - // Save the product data. - $product->save(); - - // Save variations. - if ( $product->is_type( 'variable' ) ) { - if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { - $this->save_variations_data( $product, $request ); + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); } } - // 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' ); + if ( ! empty( $request['date_created'] ) ) { + $date = rest_parse_date( $request['date_created'] ); - return true; + if ( $date ) { + $product->set_date_created( $date ); + } + } + + if ( ! empty( $request['date_created_gmt'] ) ) { + $date = rest_parse_date( $request['date_created_gmt'], true ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + if ( ! empty( $request['search'] ) ) { + $args['search'] = trim( $request['search'] ); + unset( $args['s'] ); + } + + if ( ! empty( $request['low_in_stock'] ) ) { + $args['low_in_stock'] = $request['low_in_stock']; + $args['post_type'] = array( 'product', 'product_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 $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 ); } /** - * Clear cache/transients. + * Get a collection of posts and add the post title filter option to WP_Query. * - * @param WP_Post $post Post data. + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response */ - public function clear_transients( $post ) { - wc_delete_product_transients( $post->ID ); + public function get_items( $request ) { + add_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10, 2 ); + add_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10, 2 ); + add_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10, 2 ); + $response = parent::get_items( $request ); + remove_filter( 'posts_where', array( __CLASS__, 'add_wp_query_filter' ), 10 ); + remove_filter( 'posts_join', array( __CLASS__, 'add_wp_query_join' ), 10 ); + remove_filter( 'posts_groupby', array( __CLASS__, 'add_wp_query_group_by' ), 10 ); + return $response; } /** - * Delete post. + * Add in conditional search filters for products. * - * @param int|WP_Post $id Post ID or WP_Post instance. + * @param string $where Where clause used to search posts. + * @param object $wp_query WP_Query object. + * @return string */ - protected function delete_post( $id ) { - if ( ! empty( $id->ID ) ) { - $id = $id->ID; - } elseif ( ! is_numeric( $id ) || 0 >= $id ) { - return; + public static function add_wp_query_filter( $where, $wp_query ) { + global $wpdb; + + $search = $wp_query->get( 'search' ); + if ( $search ) { + $search = $wpdb->esc_like( $search ); + $search = "'%" . $search . "%'"; + $where .= " AND ({$wpdb->posts}.post_title LIKE {$search}"; + $where .= wc_product_sku_enabled() ? ' OR ps_post_meta.meta_key = "_sku" AND ps_post_meta.meta_value LIKE ' . $search . ')' : ')'; } - // 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 ); + if ( $wp_query->get( 'low_in_stock' ) ) { + $low_stock_amount = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) ); + $where .= " AND lis_postmeta2.meta_key = '_manage_stock' + AND lis_postmeta2.meta_value = 'yes' + AND lis_postmeta.meta_key = '_stock' + AND lis_postmeta.meta_value IS NOT NULL + AND lis_postmeta3.meta_key = '_low_stock_amount' + AND ( + lis_postmeta3.meta_value > '' + AND CAST(lis_postmeta.meta_value AS SIGNED) <= CAST(lis_postmeta3.meta_value AS SIGNED) + OR lis_postmeta3.meta_value <= '' + AND CAST(lis_postmeta.meta_value AS SIGNED) <= {$low_stock_amount} + )"; } - // Delete product. - $product = wc_get_product( $id ); - $product->delete( true ); + return $where; + } + + /** + * Join posts meta tables when product search or low stock query is present. + * + * @param string $join Join clause used to search posts. + * @param object $wp_query WP_Query object. + * @return string + */ + public static function add_wp_query_join( $join, $wp_query ) { + global $wpdb; + + $search = $wp_query->get( 'search' ); + if ( $search && wc_product_sku_enabled() ) { + $join .= " INNER JOIN {$wpdb->postmeta} AS ps_post_meta ON ps_post_meta.post_id = {$wpdb->posts}.ID"; + } + + if ( $wp_query->get( 'low_in_stock' ) ) { + $join .= " INNER JOIN {$wpdb->postmeta} AS lis_postmeta ON {$wpdb->posts}.ID = lis_postmeta.post_id + INNER JOIN {$wpdb->postmeta} AS lis_postmeta2 ON {$wpdb->posts}.ID = lis_postmeta2.post_id + INNER JOIN {$wpdb->postmeta} AS lis_postmeta3 ON {$wpdb->posts}.ID = lis_postmeta3.post_id"; + } + + return $join; + } + + /** + * Group by post ID to prevent duplicates. + * + * @param string $groupby Group by clause used to organize posts. + * @param object $wp_query WP_Query object. + * @return string + */ + public static function add_wp_query_group_by( $groupby, $wp_query ) { + global $wpdb; + + $search = $wp_query->get( 'search' ); + $low_in_stock = $wp_query->get( 'low_in_stock' ); + if ( empty( $groupby ) && ( $search || $low_in_stock ) ) { + $groupby = $wpdb->posts . '.ID'; + } + return $groupby; + } + + /** + * Make extra product orderby features supported by WooCommerce available to the WC API. + * This includes 'price', 'popularity', and 'rating'. + * + * @param WP_REST_Request $request Request data. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + // Build tax_query if taxonomies are set. + if ( ! empty( $tax_query ) ) { + if ( ! empty( $args['tax_query'] ) ) { + $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. + } else { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product by stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $orderby = $request->get_param( 'orderby' ); + $order = $request->get_param( 'order' ); + + $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); + $args['orderby'] = $ordering_args['orderby']; + $args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * + * @deprecated 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get product attribute taxonomy name. + * + * @param string $slug Taxonomy name. + * @param WC_Product $product Product data. + * + * @since 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_name( $slug, $product ) { + // Format slug so it matches attributes of the product. + $slug = wc_attribute_taxonomy_slug( $slug ); + $attributes = $product->get_attributes(); + $attribute = false; + + // pa_ attributes. + if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { + $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; + } elseif ( isset( $attributes[ $slug ] ) ) { + $attribute = $attributes[ $slug ]; + } + + if ( ! $attribute ) { + return $slug; + } + + // Taxonomy attribute name. + if ( $attribute->is_taxonomy() ) { + $taxonomy = $attribute->get_taxonomy_object(); + return $taxonomy->attribute_label; + } + + // Custom product attribute name. + return $attribute->get_name(); + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( + $product_id, + $attribute['name'], + array( + 'fields' => 'names', + ) + ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + $_product = wc_get_product( $product->get_parent_id() ); + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( empty( $attribute ) && '0' !== $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, + 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. + * Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name( $context ), + 'slug' => $product->get_slug( $context ), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created( $context ), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $product->get_date_created( $context ) ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $product->get_date_modified( $context ) ), + 'type' => $product->get_type(), + 'status' => $product->get_status( $context ), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility( $context ), + 'description' => 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ), + 'short_description' => 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ), + 'sku' => $product->get_sku( $context ), + 'price' => $product->get_price( $context ), + 'regular_price' => $product->get_regular_price( $context ), + 'sale_price' => $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : '', + 'date_on_sale_from' => wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ), + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale( $context ), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales( $context ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit( $context ), + 'download_expiry' => $product->get_download_expiry( $context ), + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url( $context ) : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text( $context ) : '', + 'tax_status' => $product->get_tax_status( $context ), + 'tax_class' => $product->get_tax_class( $context ), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity( $context ), + 'stock_status' => $product->get_stock_status( $context ), + 'backorders' => $product->get_backorders( $context ), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight( $context ), + 'dimensions' => array( + 'length' => $product->get_length( $context ), + 'width' => $product->get_width( $context ), + 'height' => $product->get_height( $context ), + ), + '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( $context ), + 'reviews_allowed' => $product->get_reviews_allowed( $context ), + 'average_rating' => 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ), + '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( $context ) ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids( $context ) ), + 'parent_id' => $product->get_parent_id( $context ), + 'purchase_note' => 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ), + '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( $context ), + 'meta_data' => $product->get_meta_data(), + ); + + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. + ); + } + + return $links; + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: image ID */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $featured_image = $product->get_image_id(); + + if ( 0 === $index ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + } + + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * + * @since 3.0.0 + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); } /** * Delete a single item. * * @param WP_REST_Request $request Full details about the request. + * * @return WP_REST_Response|WP_Error */ public function delete_item( $request ) { - $id = (int) $request['id']; - $force = (bool) $request['force']; - $post = get_post( $id ); - $product = wc_get_product( $id ); + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; - if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { - return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); - } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { - return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); + 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; + if ( 'variation' === $object->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); /** - * Filter whether an item is trashable. + * Filter whether an object is trashable. * - * Return false to disable trash support for the item. + * Return false to disable trash support for the object. * - * @param boolean $supports_trash Whether the item type support trashing. - * @param WP_Post $post The Post object being considered for trashing support. + * @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}_trashable", $supports_trash, $post ); + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); - if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { - /* translators: %s: post type */ - return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( + "woocommerce_rest_user_cannot_delete_{$this->post_type}", + /* translators: %s: post type */ + sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), + array( + 'status' => rest_authorization_required_code(), + ) + ); } $request->set_param( 'context', 'edit' ); - $response = $this->prepare_item_for_response( $post, $request ); + $response = $this->prepare_object_for_response( $object, $request ); // If we're forcing, then delete permanently. if ( $force ) { - if ( $product->is_type( 'variable' ) ) { - foreach ( $product->get_children() as $child_id ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->delete( true ); @@ -1670,7 +1470,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { } } else { // For other product types, if the product has children, remove the relationship. - foreach ( $product->get_children() as $child_id ) { + foreach ( $object->get_children() as $child_id ) { $child = wc_get_product( $child_id ); if ( ! empty( $child ) ) { $child->set_parent_id( 0 ); @@ -1679,45 +1479,63 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { } } - $product->delete( true ); - $result = ! ( $product->get_id() > 0 ); + $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 ) ); + return new WP_Error( + 'woocommerce_rest_trash_not_supported', + /* translators: %s: post type */ + sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 501, + ) + ); } // Otherwise, only trash if we haven't already. - if ( 'trash' === $post->post_status ) { - /* translators: %s: post type */ - return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); - } + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + return new WP_Error( + 'woocommerce_rest_already_trashed', + /* translators: %s: post type */ + sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 410, + ) + ); + } - // (Note that internally this falls through to `wp_delete_post` if - // the trash is disabled.) - $product->delete(); - $result = 'trash' === $product->get_status(); + $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 ) ); + return new WP_Error( + 'woocommerce_rest_cannot_delete', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 500, + ) + ); } // Delete parent product transients. - if ( $parent_id = wp_get_post_parent_id( $id ) ) { - wc_delete_product_transients( $parent_id ); + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); } /** - * Fires after a single item is deleted or trashed via the REST API. + * Fires after a single object is deleted or trashed via the REST API. * - * @param object $post The deleted or trashed item. + * @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}", $post, $response, $request ); + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); return $response; } @@ -1735,153 +1553,173 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'title' => $this->post_type, 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'name' => array( + 'name' => array( 'description' => __( 'Product name.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ), - 'slug' => array( + 'slug' => array( 'description' => __( 'Product slug.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ), - 'permalink' => array( + 'permalink' => array( 'description' => __( 'Product URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), - 'date_created' => array( + 'date_created' => array( 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), - 'readonly' => true, ), - 'date_modified' => array( + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_modified' => array( 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'type' => array( + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( 'description' => __( 'Product type.', 'woocommerce' ), 'type' => 'string', 'default' => 'simple', 'enum' => array_keys( wc_get_product_types() ), 'context' => array( 'view', 'edit' ), ), - 'status' => array( + 'status' => array( 'description' => __( 'Product status (post status).', 'woocommerce' ), 'type' => 'string', 'default' => 'publish', 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), 'context' => array( 'view', 'edit' ), ), - 'featured' => array( + 'featured' => array( 'description' => __( 'Featured product.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'catalog_visibility' => array( + 'catalog_visibility' => array( 'description' => __( 'Catalog visibility.', 'woocommerce' ), 'type' => 'string', 'default' => 'visible', 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), 'context' => array( 'view', 'edit' ), ), - 'description' => array( + 'description' => array( 'description' => __( 'Product description.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ), - 'short_description' => array( + 'short_description' => array( 'description' => __( 'Product short description.', 'woocommerce' ), 'type' => 'string', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ), - 'sku' => array( + 'sku' => array( 'description' => __( 'Unique identifier.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'price' => array( + 'price' => array( 'description' => __( 'Current product price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'regular_price' => array( + 'regular_price' => array( 'description' => __( 'Product regular price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'sale_price' => array( + 'sale_price' => array( 'description' => __( 'Product sale price.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'date_on_sale_from' => array( - 'description' => __( 'Start date of sale price.', 'woocommerce' ), - 'type' => 'string', + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), - 'date_on_sale_to' => array( - 'description' => __( 'End date of sale price.', 'woocommerce' ), - 'type' => 'string', + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', 'context' => array( 'view', 'edit' ), ), - 'price_html' => array( + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'on_sale' => array( + 'on_sale' => array( 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'purchasable' => array( + 'purchasable' => array( 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'total_sales' => array( + 'total_sales' => array( 'description' => __( 'Amount of sales.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'virtual' => array( + 'virtual' => array( 'description' => __( 'If the product is virtual.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'downloadable' => array( + 'downloadable' => array( 'description' => __( 'If the product is downloadable.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'downloads' => array( + 'downloads' => array( 'description' => __( 'List of downloadable files.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'File ID.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -1899,97 +1737,91 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), ), ), - 'download_limit' => array( + 'download_limit' => array( 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), - 'download_expiry' => array( + 'download_expiry' => array( 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), 'type' => 'integer', 'default' => -1, 'context' => array( 'view', 'edit' ), ), - 'download_type' => array( - 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'standard', - 'enum' => array( 'standard' ), - 'context' => array( 'view', 'edit' ), - ), - 'external_url' => array( + 'external_url' => array( 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), - 'button_text' => array( + 'button_text' => array( 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'tax_status' => array( + 'tax_status' => array( 'description' => __( 'Tax status.', 'woocommerce' ), 'type' => 'string', 'default' => 'taxable', 'enum' => array( 'taxable', 'shipping', 'none' ), 'context' => array( 'view', 'edit' ), ), - 'tax_class' => array( + 'tax_class' => array( 'description' => __( 'Tax class.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'manage_stock' => array( + 'manage_stock' => array( 'description' => __( 'Stock management at product level.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'stock_quantity' => array( + 'stock_quantity' => array( 'description' => __( 'Stock quantity.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'in_stock' => array( - 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), 'context' => array( 'view', 'edit' ), ), - 'backorders' => array( + 'backorders' => array( 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), 'type' => 'string', 'default' => 'no', 'enum' => array( 'no', 'notify', 'yes' ), 'context' => array( 'view', 'edit' ), ), - 'backorders_allowed' => array( + 'backorders_allowed' => array( 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'backordered' => array( + 'backordered' => array( 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'sold_individually' => array( + 'sold_individually' => array( 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'weight' => array( + 'weight' => array( /* translators: %s: weight unit */ 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'dimensions' => array( + 'dimensions' => array( 'description' => __( 'Product dimensions.', 'woocommerce' ), 'type' => 'object', 'context' => array( 'view', 'edit' ), @@ -2000,7 +1832,7 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'width' => array( + 'width' => array( /* translators: %s: dimension unit */ 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), 'type' => 'string', @@ -2014,90 +1846,90 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), ), ), - 'shipping_required' => array( + 'shipping_required' => array( 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'shipping_taxable' => array( + 'shipping_taxable' => array( 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), 'type' => 'boolean', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'shipping_class' => array( + 'shipping_class' => array( 'description' => __( 'Shipping class slug.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'shipping_class_id' => array( + 'shipping_class_id' => array( 'description' => __( 'Shipping class ID.', 'woocommerce' ), - 'type' => 'integer', + 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'reviews_allowed' => array( + 'reviews_allowed' => array( 'description' => __( 'Allow reviews.', 'woocommerce' ), 'type' => 'boolean', 'default' => true, 'context' => array( 'view', 'edit' ), ), - 'average_rating' => array( + 'average_rating' => array( 'description' => __( 'Reviews average rating.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'rating_count' => array( + 'rating_count' => array( 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'related_ids' => array( + 'related_ids' => array( 'description' => __( 'List of related products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( - 'type' => 'integer', + 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'upsell_ids' => array( - 'description' => __( 'List of upsell products IDs.', 'woocommerce' ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( - 'type' => 'integer', + 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), - 'cross_sell_ids' => array( + 'cross_sell_ids' => array( 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), 'type' => 'array', 'items' => array( - 'type' => 'integer', + 'type' => 'integer', ), 'context' => array( 'view', 'edit' ), ), - 'parent_id' => array( + 'parent_id' => array( 'description' => __( 'Product parent ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'purchase_note' => array( + 'purchase_note' => array( 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'categories' => array( + 'categories' => array( 'description' => __( 'List of categories.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Category ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -2117,14 +1949,14 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), ), ), - 'tags' => array( + 'tags' => array( 'description' => __( 'List of tags.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Tag ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -2144,77 +1976,84 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), ), ), - 'images' => array( + 'images' => array( 'description' => __( 'List of images.', 'woocommerce' ), 'type' => 'object', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Image ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'date_created' => array( + 'date_created' => array( 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'date_modified' => array( + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), 'type' => 'date-time', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'src' => array( + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( 'description' => __( 'Image URL.', 'woocommerce' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit' ), ), - 'name' => array( + 'name' => array( 'description' => __( 'Image name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'alt' => array( + 'alt' => array( 'description' => __( 'Image alternative text.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'position' => array( - 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), ), ), ), - 'attributes' => array( + 'attributes' => array( 'description' => __( 'List of attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'name' => array( + 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'position' => array( + 'position' => array( 'description' => __( 'Attribute position.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'visible' => array( + 'visible' => array( 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), 'type' => 'boolean', 'default' => false, @@ -2226,27 +2065,30 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'default' => false, 'context' => array( 'view', 'edit' ), ), - 'options' => array( + 'options' => array( 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), 'context' => array( 'view', 'edit' ), ), ), ), ), - 'default_attributes' => array( + 'default_attributes' => array( 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( + 'id' => array( 'description' => __( 'Attribute ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), ), - 'name' => array( + 'name' => array( 'description' => __( 'Attribute name.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), @@ -2259,315 +2101,57 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { ), ), ), - 'variations' => array( - 'description' => __( 'List of variations.', 'woocommerce' ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 'items' => array( 'type' => 'object', 'properties' => array( - 'id' => array( - 'description' => __( 'Variation ID.', 'woocommerce' ), + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'date_created' => array( - 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'permalink' => array( - 'description' => __( 'Variation URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'sku' => array( - 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), ), - 'price' => array( - 'description' => __( 'Current variation price.', 'woocommerce' ), - 'type' => 'string', + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'regular_price' => array( - 'description' => __( 'Variation regular price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'sale_price' => array( - 'description' => __( 'Variation sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_from' => array( - 'description' => __( 'Start date of sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'date_on_sale_to' => array( - 'description' => __( 'End date of sale price.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'on_sale' => array( - 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'purchasable' => array( - 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'visible' => array( - 'description' => __( 'If the variation is visible.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - ), - 'virtual' => array( - 'description' => __( 'If the variation is virtual.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloadable' => array( - 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'downloads' => array( - 'description' => __( 'List of downloadable files.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'File ID.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'File name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'file' => array( - 'description' => __( 'File URL.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - ), - 'download_limit' => array( - 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), - 'type' => 'integer', - 'default' => null, - 'context' => array( 'view', 'edit' ), - ), - 'download_expiry' => array( - 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), - 'type' => 'integer', - 'default' => null, - 'context' => array( 'view', 'edit' ), - ), - 'tax_status' => array( - 'description' => __( 'Tax status.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'taxable', - 'enum' => array( 'taxable', 'shipping', 'none' ), - 'context' => array( 'view', 'edit' ), - ), - 'tax_class' => array( - 'description' => __( 'Tax class.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'manage_stock' => array( - 'description' => __( 'Stock management at variation level.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => false, - 'context' => array( 'view', 'edit' ), - ), - 'stock_quantity' => array( - 'description' => __( 'Stock quantity.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'in_stock' => array( - 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), - 'type' => 'boolean', - 'default' => true, - 'context' => array( 'view', 'edit' ), - ), - 'backorders' => array( - 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), - 'type' => 'string', - 'default' => 'no', - 'enum' => array( 'no', 'notify', 'yes' ), - 'context' => array( 'view', 'edit' ), - ), - 'backorders_allowed' => array( - 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'backordered' => array( - 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), - 'type' => 'boolean', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'weight' => array( - /* translators: %s: weight unit */ - 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'dimensions' => array( - 'description' => __( 'Variation dimensions.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'length' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'width' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'height' => array( - /* translators: %s: dimension unit */ - 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'shipping_class' => array( - 'description' => __( 'Shipping class slug.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'shipping_class_id' => array( - 'description' => __( 'Shipping class ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'image' => array( - 'description' => __( 'Variation image data.', 'woocommerce' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'properties' => array( - 'id' => array( - 'description' => __( 'Image ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'date_created' => array( - 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'date_modified' => array( - 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), - 'type' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'src' => array( - 'description' => __( 'Image URL.', 'woocommerce' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Image name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'alt' => array( - 'description' => __( 'Image alternative text.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'position' => array( - 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - ), - ), - 'attributes' => array( - 'description' => __( 'List of attributes.', 'woocommerce' ), - 'type' => 'array', - 'context' => array( 'view', 'edit' ), - 'items' => array( - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'Attribute ID.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), - 'name' => array( - 'description' => __( 'Attribute name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - 'option' => array( - 'description' => __( 'Selected attribute term name.', 'woocommerce' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ), - ), ), ), ), ), - 'grouped_products' => array( - 'description' => __( 'List of grouped products ID.', 'woocommerce' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'menu_order' => array( - 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - ), ), ); - return $this->add_additional_fields_schema( $schema ); } @@ -2579,12 +2163,12 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { public function get_collection_params() { $params = parent::get_collection_params(); - $params['slug'] = array( + $params['slug'] = array( 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', ); - $params['status'] = array( + $params['status'] = array( 'default' => 'any', 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), 'type' => 'string', @@ -2592,20 +2176,32 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); - $params['type'] = array( + $params['type'] = array( 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), 'type' => 'string', 'enum' => array_keys( wc_get_product_types() ), 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ); - $params['category'] = array( + $params['sku'] = array( + 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); - $params['tag'] = array( + $params['tag'] = array( 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'wp_parse_id_list', @@ -2617,8 +2213,8 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); - $params['attribute'] = array( - 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', @@ -2629,12 +2225,53 @@ class WC_REST_Products_V1_Controller extends WC_REST_Posts_Controller { 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => 'rest_validate_request_arg', ); - $params['sku'] = array( - 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + $params['on_sale'] = array( + 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['low_in_stock'] = array( + 'description' => __( 'Limit result set to products that are low or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'sanitize_callback' => 'wc_string_to_bool', + ); + $params['search'] = array( + 'description' => __( 'Search by similar product name or sku.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); return $params; }