Merge pull request #27115 from woocommerce/fix/25133

Validate variation attributes in WC_Cart::add_to_cart()
This commit is contained in:
Claudio Sanches 2020-08-17 15:02:07 -03:00 committed by GitHub
commit f968bc1f7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 109 deletions

View File

@ -1022,6 +1022,95 @@ class WC_Cart extends WC_Legacy_Cart {
return false;
}
if ( $product_data->is_type( 'variation' ) ) {
$missing_attributes = array();
$parent_data = wc_get_product( $product_data->get_parent_id() );
$variation_attributes = $product_data->get_variation_attributes();
// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
$variation_attributes = array_filter( $variation_attributes );
// Gather posted attributes.
$posted_attributes = array();
foreach ( $parent_data->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
if ( isset( $variation[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $attribute['is_taxonomy'] ) {
// Don't use wc_clean as it destroys sanitized characters.
$value = sanitize_title( wp_unslash( $variation[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$value = html_entity_decode( wc_clean( wp_unslash( $variation[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
// Don't include if it's empty.
if ( ! empty( $value ) ) {
$posted_attributes[ $attribute_key ] = $value;
}
}
}
// Merge variation attributes and posted attributes.
$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
// If no variation ID is set, attempt to get a variation ID from posted attributes.
if ( empty( $variation_id ) ) {
$data_store = WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $parent_data, $posted_attributes );
}
// Do we have a variation ID?
if ( empty( $variation_id ) ) {
throw new Exception( __( 'Please choose product options…', 'woocommerce' ) );
}
// Check the data we have is valid.
$variation_data = wc_get_product_variation_attributes( $variation_id );
$attributes = array();
foreach ( $parent_data->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
// Get valid value from variation data.
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
$valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';
/**
* If the attribute value was posted, check if it's valid.
*
* If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
*/
if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
$value = $posted_and_variation_attributes[ $attribute_key ];
// Allow if valid or show error.
if ( $valid_value === $value ) {
$attributes[ $attribute_key ] = $value;
} elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
// If valid values are empty, this is an 'any' variation so get all possible values.
$attributes[ $attribute_key ] = $value;
} else {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
}
} elseif ( '' === $valid_value ) {
$missing_attributes[] = wc_attribute_label( $attribute['name'] );
}
$variation = $attributes;
}
if ( ! empty( $missing_attributes ) ) {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
}
}
// Load cart item data - may be added by other plugins.
$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity );

View File

@ -46,7 +46,7 @@ class WC_Form_Handler {
$user = get_user_by( 'login', sanitize_user( wp_unslash( $_GET['login'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$user_id = $user ? $user->ID : 0;
} else {
$user_id = absint( $_GET['id'] );
$user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
$value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore
@ -638,7 +638,7 @@ class WC_Form_Handler {
if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) {
$cart_updated = false;
$cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // PHPCS: input var ok, CSRF ok, sanitization ok.
$cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {
@ -868,108 +868,16 @@ class WC_Form_Handler {
* @return bool success or not
*/
private static function add_to_cart_handler_variable( $product_id ) {
try {
$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$missing_attributes = array();
$variations = array();
$variation_attributes = array();
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
return false;
}
// If the $product_id was in fact a variation ID, update the variables.
if ( $adding_to_cart->is_type( 'variation' ) ) {
$variation_attributes = $adding_to_cart->get_variation_attributes();
// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
$variation_attributes = array_filter( $variation_attributes );
$variation_id = $product_id;
$product_id = $adding_to_cart->get_parent_id();
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
return false;
}
}
// Gather posted attributes.
$posted_attributes = array();
foreach ( $adding_to_cart->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
if ( isset( $_REQUEST[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $attribute['is_taxonomy'] ) {
// Don't use wc_clean as it destroys sanitized characters.
$value = sanitize_title( wp_unslash( $_REQUEST[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$value = html_entity_decode( wc_clean( wp_unslash( $_REQUEST[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
$posted_attributes[ $attribute_key ] = $value;
}
}
// Merge variation attributes and posted attributes.
$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
// If no variation ID is set, attempt to get a variation ID from posted attributes.
if ( empty( $variation_id ) ) {
$data_store = WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $adding_to_cart, $posted_attributes );
}
// Do we have a variation ID?
if ( empty( $variation_id ) ) {
throw new Exception( __( 'Please choose product options…', 'woocommerce' ) );
}
// Check the data we have is valid.
$variation_data = wc_get_product_variation_attributes( $variation_id );
foreach ( $adding_to_cart->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
continue;
}
// Get valid value from variation data.
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
$valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';
/**
* If the attribute value was posted, check if it's valid.
*
* If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
*/
if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
$value = $posted_and_variation_attributes[ $attribute_key ];
// Allow if valid or show error.
if ( $valid_value === $value ) {
$variations[ $attribute_key ] = $value;
} elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
// If valid values are empty, this is an 'any' variation so get all possible values.
$variations[ $attribute_key ] = $value;
} else {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
}
} elseif ( '' === $valid_value ) {
$missing_attributes[] = wc_attribute_label( $attribute['name'] );
}
}
if ( ! empty( $missing_attributes ) ) {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
}
} catch ( Exception $e ) {
wc_add_notice( $e->getMessage(), 'error' );
return false;
$variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
}
$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations );
@ -1083,7 +991,7 @@ class WC_Form_Handler {
return;
}
if ( in_array( $field, array( 'password_1', 'password_2' ) ) ) {
if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) {
// Don't unslash password fields
// @see https://github.com/woocommerce/woocommerce/issues/23922.
$posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash

View File

@ -1299,7 +1299,16 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Add the product to the cart. Methods returns boolean on failure, string on success.
$this->assertNotFalse( WC()->cart->add_to_cart( $product->get_id(), 1, $variation['variation_id'], array( 'Size' => ucfirst( $variation['attributes']['attribute_pa_size'] ) ) ) );
$result = WC()->cart->add_to_cart(
$product->get_id(),
1,
$variation['variation_id'],
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
$this->assertNotFalse( $result );
// Check if the item is in the cart.
$this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
@ -2252,10 +2261,10 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
// Check that the notices contain an error message about an invalid colour.
// Check that the notices contain an error message about invalid colour and number.
$this->assertArrayHasKey( 'error', $notices );
$this->assertCount( 1, $notices['error'] );
$this->assertEquals( 'colour is a required field', $notices['error'][0]['notice'] );
$this->assertEquals( 'colour and number are required fields', $notices['error'][0]['notice'] );
}
/**

View File

@ -279,7 +279,15 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$variation->set_manage_stock( true );
$variation->set_stock_quantity( 10 );
$variation->save();
WC()->cart->add_to_cart( $variation->get_id(), 9 );
WC()->cart->add_to_cart(
$variation->get_id(),
9,
0,
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
@ -299,7 +307,15 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$this->assertEquals( 9, wc_get_held_stock_quantity( $variation ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $variation->get_stock_managed_by_id(), 2 );
WC()->cart->add_to_cart(
$variation->get_stock_managed_by_id(),
2,
0,
array(
'attribute_pa_colour' => 'red',
'attribute_pa_number' => '2',
)
);
$this->assertEquals( false, WC()->cart->check_cart_items() );
}

View File

@ -88,7 +88,15 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product2->get_id(), 2 );
$variations = $product3->get_available_variations();
$variation = array_shift( $variations );
WC()->cart->add_to_cart( $product3->get_id(), 1, $variation['variation_id'], array( 'Size' => ucfirst( $variation['attributes']['attribute_pa_size'] ) ) );
WC()->cart->add_to_cart(
$product3->get_id(),
1,
$variation['variation_id'],
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
WC()->cart->add_discount( $coupon->get_code() );

View File

@ -21,6 +21,46 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
WC()->session->set( 'wc_notices', null );
}
/**
* @testdox should throw a notice to the cart if an "any" attribute is empty.
*/
public function test_add_variation_to_the_cart_with_empty_attributes() {
WC()->cart->empty_cart();
WC()->session->set( 'wc_notices', null );
$product = WC_Helper_Product::create_variation_product();
$variations = $product->get_available_variations();
// Get a variation with small pa_size and any pa_colour and pa_number.
$variation = $variations[0];
// Add variation using parent id.
WC()->cart->add_to_cart(
$variation['variation_id'],
1,
0,
array(
'attribute_pa_colour' => '',
'attribute_pa_number' => '',
)
);
$notices = WC()->session->get( 'wc_notices', array() );
// Check that the second add to cart call increases the quantity of the existing cart-item.
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
// Check that the notices contain an error message about invalid colour and number.
$this->assertArrayHasKey( 'error', $notices );
$this->assertCount( 1, $notices['error'] );
$this->assertEquals( 'colour and number are required fields', $notices['error'][0]['notice'] );
// Reset cart.
WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
$product->delete( true );
}
/**
* Test show shipping.
*/