diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index 4601d7af77e..997fe7f9418 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -235,7 +235,7 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { $meta_fields = $this->add_post_meta_fields( $post, $request ); if ( is_wp_error( $meta_fields ) ) { // Remove post. - wp_delete_post( $post->ID, true ); + $this->delete_post( $post ); return $meta_fields; } @@ -269,6 +269,15 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return true; } + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + wp_delete_post( $post->ID, true ); + } + /** * Update a single post. * diff --git a/includes/api/class-wc-rest-products-controller.php b/includes/api/class-wc-rest-products-controller.php index 109b9bbe91e..bf74c8f4fc7 100644 --- a/includes/api/class-wc-rest-products-controller.php +++ b/includes/api/class-wc-rest-products-controller.php @@ -48,6 +48,7 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { */ 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' ) ); } /** @@ -125,10 +126,10 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $data = array( 'id' => (int) $product->is_type( 'variation' ) ? $product->get_variation_id() : $product->id, 'name' => $product->get_title(), - 'slug' => $product->get_post_data()->name, + 'slug' => $product->get_post_data()->post_name, 'permalink' => $product->get_permalink(), - 'date_created' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $product->get_post_data()->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $product->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_post_data()->post_modified_gmt ), 'type' => $product->product_type, 'status' => $product->get_post_data()->post_status, 'featured' => $product->is_featured(), @@ -208,8 +209,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $variations[] = array( 'id' => $variation->get_variation_id(), - 'date_created' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), + 'date_created' => wc_rest_prepare_date_response( $variation->get_post_data()->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_post_data()->post_modified_gmt ), 'permalink' => $variation->get_permalink(), 'sku' => $variation->get_sku(), 'price' => $variation->get_price(), @@ -309,8 +310,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { $images[] = array( 'id' => (int) $attachment_id, - 'date_created' => wc_rest_api_prepare_date_response( $attachment_post->post_date_gmt ), - 'date_modified' => wc_rest_api_prepare_date_response( $attachment_post->post_modified_gmt ), + '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 ), 'title' => get_the_title( $attachment_id ), 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), @@ -322,8 +323,8 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { if ( empty( $images ) ) { $images[] = array( 'id' => 0, - 'date_created' => wc_rest_api_prepare_date_response( time() ), // Default to now. - 'date_modified' => wc_rest_api_prepare_date_response( time() ), + 'date_created' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( time() ), 'src' => wc_placeholder_img_src(), 'title' => __( 'Placeholder', 'woocommerce' ), 'alt' => __( 'Placeholder', 'woocommerce' ), @@ -466,6 +467,1097 @@ class WC_REST_Products_Controller extends WC_REST_Posts_Controller { return $links; } + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $data->post_title = wp_filter_post_kses( $request['name'] ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $data->post_content = wp_filter_post_kses( $request['description'] ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['short_description'] ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $data->post_status = get_post_status_object( $request['status'] ) ? $request['status'] : 'draft'; + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $data->post_name = $request['slug']; + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $data->menu_order = (int) $request['menu_order']; + } + + // Comment status. + if ( ! empty( $request['reviews_allowed'] ) ) { + $data->comment_status = $request['reviews_allowed'] ? 'open' : 'closed'; + } + + // Only when creating products. + if ( empty( $request['id'] ) ) { + // Post type. + $data->post_type = $this->post_type; + + // Ping status. + $data->ping_status = 'closed'; + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Get attribute taxonomy by slug. + * + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Save product images. + * + * @param WC_Product $product + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && $image['position'] == 0 ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + } + + set_post_thumbnail( $product->id, $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + update_post_meta( $product->id, '_product_image_gallery', implode( ',', $gallery ) ); + } + } else { + delete_post_thumbnail( $product->id ); + update_post_meta( $product->id, '_product_image_gallery', '' ); + } + } + + /** + * Save product shipping data. + * + * @param WC_Product $product + * @param array $data + */ + private function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + update_post_meta( $product->id, '_weight', '' ); + update_post_meta( $product->id, '_length', '' ); + update_post_meta( $product->id, '_width', '' ); + update_post_meta( $product->id, '_height', '' ); + } else { + if ( isset( $data['weight'] ) ) { + update_post_meta( $product->id, '_weight', '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Height. + if ( isset( $data['height'] ) ) { + update_post_meta( $product->id, '_height', '' === $data['height'] ? '' : wc_format_decimal( $data['height'] ) ); + } + + // Width. + if ( isset( $data['width'] ) ) { + update_post_meta( $product->id, '_width', '' === $data['width'] ? '' : wc_format_decimal($data['width'] ) ); + } + + // Length. + if ( isset( $data['length'] ) ) { + update_post_meta( $product->id, '_length', '' === $data['length'] ? '' : wc_format_decimal( $data['length'] ) ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + wp_set_object_terms( $product->id, wc_clean( $data['shipping_class'] ), 'product_shipping_class' ); + } + } + + /** + * Save downloadable files. + * + * @param WC_Product $product + * @param array $downloads + * @param int $variation_id + */ + private function save_downloadable_files( $product, $downloads, $variation_id = 0 ) { + $files = array(); + + // File paths will be stored in an array keyed off md5(file path). + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( ! isset( $file['file'] ) ) { + continue; + } + + $file_name = isset( $file['name'] ) ? wc_clean( $file['name'] ) : ''; + + if ( 0 === strpos( $file['file'], 'http' ) ) { + $file_url = esc_url_raw( $file['file'] ); + } else { + $file_url = wc_clean( $file['file'] ); + } + + $files[ md5( $file_url ) ] = array( + 'name' => $file_name, + 'file' => $file_url + ); + } + + // Grant permission to any newly added files on any existing orders for this product prior to saving. + do_action( 'woocommerce_process_product_file_download_paths', $product->id, $variation_id, $files ); + + $id = ( 0 === $variation_id ) ? $product->id : $variation_id; + + update_post_meta( $id, '_downloadable_files', $files ); + } + + /** + * Save product meta. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + global $wpdb; + + // Product Type. + $product_type = null; + if ( isset( $request['type'] ) ) { + $product_type = wc_clean( $request['type'] ); + wp_set_object_terms( $product->id, $product_type, 'product_type' ); + } else { + $_product_type = get_the_terms( $product->id, 'product_type' ); + if ( is_array( $_product_type ) ) { + $_product_type = current( $_product_type ); + $product_type = $_product_type->slug; + } + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + update_post_meta( $product->id, '_virtual', true === $request['virtual'] ? 'yes' : 'no' ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + update_post_meta( $product->id, '_tax_status', wc_clean( $request['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + update_post_meta( $product->id, '_tax_class', wc_clean( $request['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + update_post_meta( $product->id, '_visibility', wc_clean( $request['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + update_post_meta( $product->id, '_purchase_note', wc_clean( $request['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + update_post_meta( $product->id, '_featured', true === $request['featured'] ? 'yes' : 'no' ); + } + + // Shipping data. + $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $sku = get_post_meta( $product->id, '_sku', true ); + $new_sku = wc_clean( $request['sku'] ); + + if ( '' == $new_sku ) { + update_post_meta( $product->id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + update_post_meta( $product->id, '_sku', $new_sku ); + } + } else { + update_post_meta( $product->id, '_sku', '' ); + } + } + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms. + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->id, $values, $taxonomy ); + } + + if ( $values ) { + // Add attribute to array, but don't set values. + $attributes[ $taxonomy ] = array( + 'name' => $taxonomy, + 'value' => '', + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', $attribute['options'] ) ); + + // Text based, separate by pipe. + } else { + $values = implode( ' ' . WC_DELIMITER . ' ', array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attributes[ $attribute_slug ] = array( + 'name' => wc_clean( $attribute['name'] ), + 'value' => $values, + 'position' => isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0, + 'is_visible' => ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0, + 'is_variation' => ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0, + 'is_taxonomy' => $is_taxonomy + ); + } + } + + if ( ! function_exists( 'attributes_cmp' ) ) { + function attributes_cmp( $a, $b ) { + if ( $a['position'] == $b['position'] ) { + return 0; + } + + return ( $a['position'] < $b['position'] ) ? -1 : 1; + } + } + uasort( $attributes, 'attributes_cmp' ); + + update_post_meta( $product->id, '_product_attributes', $attributes ); + } + + // Sales and prices. + if ( in_array( $product_type, array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + update_post_meta( $product->id, '_regular_price', '' ); + update_post_meta( $product->id, '_sale_price', '' ); + update_post_meta( $product->id, '_sale_price_dates_from', '' ); + update_post_meta( $product->id, '_sale_price_dates_to', '' ); + update_post_meta( $product->id, '_price', '' ); + + } else { + + // Regular Price + if ( isset( $request['regular_price'] ) ) { + $regular_price = ( '' === $request['regular_price'] ) ? '' : $request['regular_price']; + } else { + $regular_price = get_post_meta( $product->id, '_regular_price', true ); + } + + // Sale Price + if ( isset( $request['sale_price'] ) ) { + $sale_price = ( '' === $request['sale_price'] ) ? '' : $request['sale_price']; + } else { + $sale_price = get_post_meta( $product->id, '_sale_price', true ); + } + + if ( isset( $request['sale_price_dates_from'] ) ) { + $date_from = $request['sale_price_dates_from']; + } else { + $date_from = get_post_meta( $product->id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $request['sale_price_dates_to'] ) ) { + $date_to = $request['sale_price_dates_to']; + } else { + $date_to = get_post_meta( $product->id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $product->id, $regular_price, $sale_price, $date_from, $date_to ); + } + + // Product parent ID for groups. + $parent_id = 0; + if ( isset( $request['parent_id'] ) ) { + $parent_id = wp_update_post( array( 'ID' => $product->id, 'post_parent' => absint( $request['parent_id'] ) ) ); + } + + // Update parent if grouped so price sorting works and stays in sync with the cheapest child. + if ( $parent_id > 0 || $product_type == 'grouped' ) { + + $clear_parent_ids = array(); + + if ( $parent_id > 0 ) { + $clear_parent_ids[] = $parent_id; + } + + if ( $product_type == 'grouped' ) { + $clear_parent_ids[] = $product->id; + } + + if ( $clear_parent_ids ) { + foreach ( $clear_parent_ids as $clear_id ) { + + $children_by_price = get_posts( array( + 'post_parent' => $clear_id, + 'orderby' => 'meta_value_num', + 'order' => 'asc', + 'meta_key' => '_price', + 'posts_per_page' => 1, + 'post_type' => 'product', + 'fields' => 'ids' + ) ); + + if ( $children_by_price ) { + foreach ( $children_by_price as $child ) { + $child_price = get_post_meta( $child, '_price', true ); + update_post_meta( $clear_id, '_price', $child_price ); + } + } + } + } + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + update_post_meta( $product->id, '_sold_individually', true === $request['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = get_post_meta( $product->id, '_stock_status', true ); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['managing_stock'] ) ) { + $managing_stock = ( true === $request['managing_stock'] ) ? 'yes' : 'no'; + update_post_meta( $product->id, '_manage_stock', $managing_stock ); + } else { + $managing_stock = get_post_meta( $product->id, '_manage_stock', true ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + if ( 'notify' === $request['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $request['backorders'] ) ? 'yes' : 'no'; + } + + update_post_meta( $product->id, '_backorders', $backorders ); + } else { + $backorders = get_post_meta( $product->id, '_backorders', true ); + } + + if ( 'grouped' == $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } elseif ( 'external' == $product_type ) { + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', 'no' ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + update_post_meta( $product->id, '_backorders', $backorders ); + + wc_update_product_stock_status( $product->id, $stock_status ); + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + wc_update_product_stock( $product->id, wc_stock_amount( $request['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $product->id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $product->id, wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + update_post_meta( $product->id, '_manage_stock', 'no' ); + update_post_meta( $product->id, '_backorders', $backorders ); + update_post_meta( $product->id, '_stock', '' ); + + wc_update_product_stock_status( $product->id, $stock_status ); + } + + } else { + wc_update_product_stock_status( $product->id, $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + update_post_meta( $product->id, '_upsell_ids', $upsells ); + } else { + delete_post_meta( $product->id, '_upsell_ids' ); + } + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + update_post_meta( $product->id, '_crosssell_ids', $crosssells ); + } else { + delete_post_meta( $product->id, '_crosssell_ids' ); + } + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $term_ids = array_unique( array_map( 'intval', $request['categories'] ) ); + wp_set_object_terms( $product->id, $term_ids, 'product_cat' ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $term_ids = array_unique( array_map( 'intval', $request['tags'] ) ); + wp_set_object_terms( $product->id, $term_ids, 'product_tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $is_downloadable = true === $request['downloadable'] ? 'yes' : 'no'; + update_post_meta( $product->id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $product->id, '_downloadable', true ); + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + update_post_meta( $product->id, '_download_limit', ( '' === $request['download_limit'] ) ? '' : absint( $request['download_limit'] ) ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + update_post_meta( $product->id, '_download_expiry', ( '' === $request['download_expiry'] ) ? '' : absint( $request['download_expiry'] ) ); + } + + // Download type. + if ( isset( $request['download_type'] ) ) { + update_post_meta( $product->id, '_download_type', wc_clean( $request['download_type'] ) ); + } + } + + // Product url and button text for external products. + if ( $product_type == 'external' ) { + if ( isset( $request['external_url'] ) ) { + update_post_meta( $product->id, '_product_url', wc_clean( $request['external_url'] ) ); + } + + if ( isset( $request['button_text'] ) ) { + update_post_meta( $product->id, '_button_text', wc_clean( $request['button_text'] ) ); + } + } + + return true; + } + + /** + * Save variations. + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $variations = $request['variations']; + $attributes = (array) maybe_unserialize( get_post_meta( $product->id, '_product_attributes', true ) ); + + foreach ( $variations as $menu_order => $variation ) { + $variation_id = isset( $variation['id'] ) ? absint( $variation['id'] ) : 0; + + // Generate a useful post title. + $variation_post_title = sprintf( __( 'Variation #%s of %s', 'woocommerce' ), $variation_id, esc_html( get_the_title( $product->id ) ) ); + + // Update or Add post. + if ( ! $variation_id ) { + $post_status = ( isset( $variation['visible'] ) && false === $variation['visible'] ) ? 'private' : 'publish'; + + $new_variation = array( + 'post_title' => $variation_post_title, + 'post_content' => '', + 'post_status' => $post_status, + 'post_author' => get_current_user_id(), + 'post_parent' => $product->id, + 'post_type' => 'product_variation', + 'menu_order' => $menu_order + ); + + $variation_id = wp_insert_post( $new_variation ); + + do_action( 'woocommerce_create_product_variation', $variation_id ); + } else { + $update_variation = array( 'post_title' => $variation_post_title, 'menu_order' => $menu_order ); + if ( isset( $variation['visible'] ) ) { + $post_status = ( false === $variation['visible'] ) ? 'private' : 'publish'; + $update_variation['post_status'] = $post_status; + } + + $wpdb->update( $wpdb->posts, $update_variation, array( 'ID' => $variation_id ) ); + + do_action( 'woocommerce_update_product_variation', $variation_id ); + } + + // Stop with we don't have a variation ID. + if ( is_wp_error( $variation_id ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_save_product_variation', $variation_id->get_error_message(), 400 ); + } + + // SKU. + if ( isset( $variation['sku'] ) ) { + $sku = get_post_meta( $variation_id, '_sku', true ); + $new_sku = wc_clean( $variation['sku'] ); + + if ( '' == $new_sku ) { + update_post_meta( $variation_id, '_sku', '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $variation_id, $new_sku ); + if ( ! $unique_sku ) { + throw new WC_REST_Exception( 'woocommerce_rest_product_sku_already_exists', __( 'The SKU already exists on another product', 'woocommerce' ), 400 ); + } else { + update_post_meta( $variation_id, '_sku', $new_sku ); + } + } else { + update_post_meta( $variation_id, '_sku', '' ); + } + } + } + + // Thumbnail. + if ( isset( $variation['image'] ) && is_array( $variation['image'] ) ) { + $image = current( $variation['image'] ); + if ( $image && is_array( $image ) ) { + if ( isset( $image['position'] ) && isset( $image['src'] ) && $image['position'] == 0 ) { + $upload = wc_rest_upload_image_from_url( wc_clean( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->id ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + + update_post_meta( $variation_id, '_thumbnail_id', $attachment_id ); + } + } else { + delete_post_meta( $variation_id, '_thumbnail_id' ); + } + } + + // Virtual variation. + if ( isset( $variation['virtual'] ) ) { + $is_virtual = ( true === $variation['virtual'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_virtual', $is_virtual ); + } + + // Downloadable variation. + if ( isset( $variation['downloadable'] ) ) { + $is_downloadable = ( true === $variation['downloadable'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_downloadable', $is_downloadable ); + } else { + $is_downloadable = get_post_meta( $variation_id, '_downloadable', true ); + } + + // Shipping data. + $this->save_product_shipping_data( $variation_id, $variation ); + + // Stock handling. + if ( isset( $variation['managing_stock'] ) ) { + $managing_stock = ( true === $variation['managing_stock'] ) ? 'yes' : 'no'; + update_post_meta( $variation_id, '_manage_stock', $managing_stock ); + } else { + $managing_stock = get_post_meta( $variation_id, '_manage_stock', true ); + } + + // Only update stock status to user setting if changed by the user, + // but do so before looking at stock levels at variation level. + if ( isset( $variation['in_stock'] ) ) { + $stock_status = ( true === $variation['in_stock'] ) ? 'instock' : 'outofstock'; + wc_update_product_stock_status( $variation_id, $stock_status ); + } + + if ( 'yes' === $managing_stock ) { + $backorders = get_post_meta( $variation_id, '_backorders', true ); + + if ( isset( $variation['backorders'] ) ) { + if ( 'notify' == $variation['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $variation['backorders'] ) ? 'yes' : 'no'; + } + } + + update_post_meta( $variation_id, '_backorders', '' === $backorders ? 'no' : $backorders ); + + if ( isset( $variation['stock_quantity'] ) ) { + wc_update_product_stock( $variation_id, wc_stock_amount( $variation['stock_quantity'] ) ); + } else if ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( get_post_meta( $variation_id, '_stock', true ) ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + + wc_update_product_stock( $variation_id, wc_stock_amount( $stock_quantity ) ); + } + } else { + delete_post_meta( $variation_id, '_backorders' ); + delete_post_meta( $variation_id, '_stock' ); + } + + // Regular Price. + if ( isset( $variation['regular_price'] ) ) { + $regular_price = ( '' === $variation['regular_price'] ) ? '' : $variation['regular_price']; + } else { + $regular_price = get_post_meta( $variation_id, '_regular_price', true ); + } + + // Sale Price. + if ( isset( $variation['sale_price'] ) ) { + $sale_price = ( '' === $variation['sale_price'] ) ? '' : $variation['sale_price']; + } else { + $sale_price = get_post_meta( $variation_id, '_sale_price', true ); + } + + if ( isset( $variation['sale_price_dates_from'] ) ) { + $date_from = $variation['sale_price_dates_from']; + } else { + $date_from = get_post_meta( $variation_id, '_sale_price_dates_from', true ); + $date_from = ( '' === $date_from ) ? '' : date( 'Y-m-d', $date_from ); + } + + if ( isset( $variation['sale_price_dates_to'] ) ) { + $date_to = $variation['sale_price_dates_to']; + } else { + $date_to = get_post_meta( $variation_id, '_sale_price_dates_to', true ); + $date_to = ( '' === $date_to ) ? '' : date( 'Y-m-d', $date_to ); + } + + _wc_save_product_price( $variation_id, $regular_price, $sale_price, $date_from, $date_to ); + + // Tax class. + if ( isset( $variation['tax_class'] ) ) { + if ( $variation['tax_class'] !== 'parent' ) { + update_post_meta( $variation_id, '_tax_class', wc_clean( $variation['tax_class'] ) ); + } else { + delete_post_meta( $variation_id, '_tax_class' ); + } + } + + // Downloads. + if ( 'yes' == $is_downloadable ) { + // Downloadable files. + if ( isset( $variation['downloads'] ) && is_array( $variation['downloads'] ) ) { + $this->save_downloadable_files( $product->id, $variation['downloads'], $variation_id ); + } + + // Download limit. + if ( isset( $variation['download_limit'] ) ) { + $download_limit = absint( $variation['download_limit'] ); + update_post_meta( $variation_id, '_download_limit', ( ! $download_limit ) ? '' : $download_limit ); + } + + // Download expiry. + if ( isset( $variation['download_expiry'] ) ) { + $download_expiry = absint( $variation['download_expiry'] ); + update_post_meta( $variation_id, '_download_expiry', ( ! $download_expiry ) ? '' : $download_expiry ); + } + } else { + update_post_meta( $variation_id, '_download_limit', '' ); + update_post_meta( $variation_id, '_download_expiry', '' ); + update_post_meta( $variation_id, '_downloadable_files', '' ); + } + + // Description. + if ( isset( $variation['description'] ) ) { + update_post_meta( $variation_id, '_variation_description', wp_kses_post( $variation['description'] ) ); + } + + // Update taxonomies. + if ( isset( $variation['attributes'] ) ) { + $updated_attribute_keys = array(); + + foreach ( $variation['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = 'attribute_' . sanitize_title( $_attribute['name'] ); + $updated_attribute_keys[] = $_attribute_key; + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + update_post_meta( $variation_id, $_attribute_key, $_attribute_value ); + } + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( $wpdb->prepare( "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE 'attribute_%%' AND meta_key NOT IN ( '" . implode( "','", $updated_attribute_keys ) . "' ) AND post_id = %d;", $variation_id ) ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $variation_id, $key ); + } + } + + do_action( 'woocommerce_rest_save_product_variation', $variation_id, $menu_order, $variation ); + } + + // Update parent if variable so price sorting works and stays in sync with the cheapest child. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + update_post_meta( $product->id, '_default_attributes', $default_attributes ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( isset( $request['type'] ) && 'variable' == $request['type'] && isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations( $product, $request ); + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + try { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $this->save_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $this->save_product_meta( $product, $request ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations( $product, $request ); + } else { + // Just sync variations. + WC_Product_Variable::sync( $product->id ); + WC_Product_Variable::sync_stock_status( $product->id ); + } + } + + return true; + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param WP_Post $post + */ + protected function delete_post( $post ) { + // Delete product attachments. + $attachments = get_children( array( + 'post_parent' => $post->ID, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + wp_delete_post( $post->ID, true ); + } + /** * Get the Product's schema, conforming to JSON Schema. *