roduct ) { throw new RouteException( 'woocommerce_rest_product_not_purchasable', sprintf( /* translators: %s: product name */ __( '"%s" is not available for purchase.', 'woocommerce' ), $product->get_name() ), 400 ); } /** * Filter data for add to cart requests. * * @param array $request Add to cart request params. * @return array Updated request array. */ protected function filter_request_data( $request ) { $product_id = $request['id']; $variation_id = 0; $product = wc_get_product( $product_id ); if ( $product->is_type( 'variation' ) ) { $product_id = $product->get_parent_id(); $variation_id = $product->get_id(); } /** * Filter cart item data for add to cart requests. * * @since 2.5.0 * * @internal Matches filter name in WooCommerce core. * * @param array $cart_item_data Array of other cart item data. * @param integer $product_id ID of the product added to the cart. * @param integer $variation_id Variation ID of the product added to the cart. * @param integer $quantity Quantity of the item added to the cart. * @return array */ $request['cart_item_data'] = (array) apply_filters( 'woocommerce_add_cart_item_data', $request['cart_item_data'], $product_id, $variation_id, $request['quantity'] ); if ( $product->is_sold_individually() ) { /** * Filter sold individually quantity for add to cart requests. * * @since 2.5.0 * * @internal Matches filter name in WooCommerce core. * * @param integer $sold_individually_quantity Defaults to 1. * @param integer $quantity Quantity of the item added to the cart. * @param integer $product_id ID of the product added to the cart. * @param integer $variation_id Variation ID of the product added to the cart. * @param array $cart_item_data Array of other cart item data. * @return integer */ $request['quantity'] = apply_filters( 'woocommerce_add_to_cart_sold_individually_quantity', 1, $request['quantity'], $product_id, $variation_id, $request['cart_item_data'] ); } return $request; } /** * If variations are set, validate and format the values ready to add to the cart. * * @throws RouteException Exception if invalid data is detected. * * @param array $request Add to cart request params. * @return array Updated request array. */ protected function parse_variation_data( $request ) { $product = $this->get_product_for_cart( $request ); // Remove variation request if not needed. if ( ! $product->is_type( array( 'variation', 'variable' ) ) ) { $request['variation'] = []; return $request; } // Flatten data and format posted values. $variable_product_attributes = $this->get_variable_product_attributes( $product ); $request['variation'] = $this->sanitize_variation_data( wp_list_pluck( $request['variation'], 'value', 'attribute' ), $variable_product_attributes ); // If we have a parent product, find the variation ID. if ( $product->is_type( 'variable' ) ) { $request['id'] = $this->get_variation_id_from_variation_data( $request, $product ); } // Now we have a variation ID, get the valid set of attributes for this variation. They will have an attribute_ prefix since they are from meta. $expected_attributes = wc_get_product_variation_attributes( $request['id'] ); $missing_attributes = []; foreach ( $variable_product_attributes as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } $prefixed_attribute_name = 'attribute_' . sanitize_title( $attribute['name'] ); $expected_value = isset( $expected_attributes[ $prefixed_attribute_name ] ) ? $expected_attributes[ $prefixed_attribute_name ] : ''; $attribute_label = wc_attribute_label( $attribute['name'] ); if ( isset( $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ] ) ) { $given_value = $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ]; if ( $expected_value === $given_value ) { continue; } // If valid values are empty, this is an 'any' variation so get all possible values. if ( '' === $expected_value && in_array( $given_value, $attribute->get_slugs(), true ) ) { continue; } throw new RouteException( 'woocommerce_rest_invalid_variation_data', /* translators: %1$s: Attribute name, %2$s: Allowed values. */ sprintf( __( 'Invalid value posted for %1$s. Allowed values: %2$s', 'woocommerce' ), $attribute_label, implode( ', ', $attribute->get_slugs() ) ), 400 ); } // Fills request array with unspecified attributes that have default values. This ensures the variation always has full data. if ( '' !== $expected_value && ! isset( $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ] ) ) { $request['variation'][ wc_variation_attribute_name( $attribute['name'] ) ] = $expected_value; } // If no attribute was posted, only error if the variation has an 'any' attribute which requires a value. if ( '' === $expected_value ) { $missing_attributes[] = $attribute_label; } } if ( ! empty( $missing_attributes ) ) { throw new RouteException( 'woocommerce_rest_missing_variation_data', /* translators: %s: Attribute name. */ __( 'Missing variation data for variable product.', 'woocommerce' ) . ' ' . sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ), 400 ); } ksort( $request['variation'] ); return $request; } /** * Try to match request data to a variation ID and return the ID. * * @throws RouteException Exception if variation cannot be found. * * @param array $request Add to cart request params. * @param \WC_Product $product Product being added to the cart. * @return int Matching variation ID. */ protected function get_variation_id_from_variation_data( $request, $product ) { $data_store = \WC_Data_Store::load( 'product' ); $match_attributes = $request['variation']; $variation_id = $data_store->find_matching_product_variation( $product, $match_attributes ); if ( empty( $variation_id ) ) { throw new RouteException( 'woocommerce_rest_variation_id_from_variation_data', __( 'No matching variation found.', 'woocommerce' ), 400 ); } return $variation_id; } /** * Format and sanitize variation data posted to the API. * * Labels are converted to names (e.g. Size to pa_size), and values are cleaned. * * @throws RouteException Exception if variation cannot be found. * * @param array $variation_data Key value pairs of attributes and values. * @param array $variable_product_attributes Product attributes we're expecting. * @return array */ protected function sanitize_variation_data( $variation_data, $variable_product_attributes ) { $return = []; foreach ( $variable_product_attributes as $attribute ) { if ( ! $attribute['is_variation'] ) { continue; } $attribute_label = wc_attribute_label( $attribute['name'] ); $variation_attribute_name = wc_variation_attribute_name( $attribute['name'] ); // Attribute labels e.g. Size. if ( isset( $variation_data[ $attribute_label ] ) ) { $return[ $variation_attribute_name ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute_label ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute_label ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); continue; } // Attribute slugs e.g. pa_size. if ( isset( $variation_data[ $attribute['name'] ] ) ) { $return[ $variation_attribute_name ] = $attribute['is_taxonomy'] ? sanitize_title( $variation_data[ $attribute['name'] ] ) : html_entity_decode( wc_clean( $variation_data[ $attribute['name'] ] ), ENT_QUOTES, get_bloginfo( 'charset' ) ); } } return $return; } /** * Get product attributes from the variable product (which may be the parent if the product object is a variation). * * @throws RouteException Exception if product is invalid. * * @param \WC_Product $product Product being added to the cart. * @return array */ protected function get_variable_product_attributes( $product ) { if ( $product->is_type( 'variation' ) ) { $product = wc_get_product( $product->get_parent_id() ); } if ( ! $product || 'trash' === $product->get_status() ) { throw new RouteException( 'woocommerce_rest_cart_invalid_parent_product', __( 'This product cannot be added to the cart.', 'woocommerce' ), 400 ); } return $product->get_attributes(); } }