Merge pull request #28849 from woocommerce/fix/27521-2

Verify country code on checkout
This commit is contained in:
Vedanshu Jain 2021-02-11 18:59:04 +05:30 committed by GitHub
commit 5a707f3e74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 198 additions and 24 deletions

View File

@ -688,23 +688,25 @@ class WC_Checkout {
foreach ( $fieldset as $key => $field ) { foreach ( $fieldset as $key => $field ) {
$type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' ); $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' );
// phpcs:disable WordPress.Security.NonceVerification.Missing
switch ( $type ) { switch ( $type ) {
case 'checkbox': case 'checkbox':
$value = isset( $_POST[ $key ] ) ? 1 : ''; // WPCS: input var ok, CSRF ok. $value = isset( $_POST[ $key ] ) ? 1 : '';
break; break;
case 'multiselect': case 'multiselect':
$value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( wp_unslash( $_POST[ $key ] ) ) ) : ''; // WPCS: input var ok, CSRF ok. $value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( wp_unslash( $_POST[ $key ] ) ) ) : '';
break; break;
case 'textarea': case 'textarea':
$value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok. $value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( wp_unslash( $_POST[ $key ] ) ) : '';
break; break;
case 'password': case 'password':
$value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. $value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
break; break;
default: default:
$value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok. $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : '';
break; break;
} }
// phpcs:enable WordPress.Security.NonceVerification.Missing
$data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) ); $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) );
} }
@ -744,6 +746,13 @@ class WC_Checkout {
$format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() ); $format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() );
$field_label = isset( $field['label'] ) ? $field['label'] : ''; $field_label = isset( $field['label'] ) ? $field['label'] : '';
if ( $validate_fieldset &&
'country' === $field['type'] &&
! WC()->countries->country_exists( $data[ $key ] ) ) {
/* translators: ISO 3166-1 alpha-2 country code */
$errors->add( $key . '_validation', sprintf( __( "'%s' is not a valid country code.", 'woocommerce' ), $data[ $key ] ) );
}
switch ( $fieldset_key ) { switch ( $fieldset_key ) {
case 'shipping': case 'shipping':
/* translators: %s: field name */ /* translators: %s: field name */
@ -830,18 +839,21 @@ class WC_Checkout {
$this->validate_posted_data( $data, $errors ); $this->validate_posted_data( $data, $errors );
$this->check_cart_items(); $this->check_cart_items();
if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) { // WPCS: input var ok, CSRF ok. // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) {
$errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) ); $errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) );
} }
if ( WC()->cart->needs_shipping() ) { if ( WC()->cart->needs_shipping() ) {
$shipping_country = WC()->customer->get_shipping_country(); $shipping_country = isset( $data['shipping_country'] ) ? $data['shipping_country'] : WC()->customer->get_shipping_country();
if ( empty( $shipping_country ) ) { if ( empty( $shipping_country ) ) {
$errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) ); $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) );
} elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ), true ) ) { } elseif ( ! in_array( $shipping_country, array_keys( WC()->countries->get_shipping_countries() ), true ) ) {
/* translators: %s: shipping location */ if ( WC()->countries->country_exists( $shipping_country ) ) {
$errors->add( 'shipping', sprintf( __( 'Unfortunately <strong>we do not ship %s</strong>. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . WC()->customer->get_shipping_country() ) ); /* translators: %s: shipping location (prefix e.g. 'to' + ISO 3166-1 alpha-2 country code) */
$errors->add( 'shipping', sprintf( __( 'Unfortunately <strong>we do not ship %s</strong>. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . $shipping_country ) );
}
} else { } else {
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
@ -963,7 +975,7 @@ class WC_Checkout {
$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
if ( ! is_ajax() ) { if ( ! is_ajax() ) {
wp_redirect( $result['redirect'] ); wp_safe_redirect( $result['redirect'] );
exit; exit;
} }
@ -1203,8 +1215,8 @@ class WC_Checkout {
*/ */
public function get_value( $input ) { public function get_value( $input ) {
// If the form was posted, get the posted value. This will only tend to happen when JavaScript is disabled client side. // If the form was posted, get the posted value. This will only tend to happen when JavaScript is disabled client side.
if ( ! empty( $_POST[ $input ] ) ) { // WPCS: input var ok, CSRF OK. if ( ! empty( $_POST[ $input ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return wc_clean( wp_unslash( $_POST[ $input ] ) ); // WPCS: input var ok, CSRF OK. return wc_clean( wp_unslash( $_POST[ $input ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
} }
// Allow 3rd parties to short circuit the logic and return their own default value. // Allow 3rd parties to short circuit the logic and return their own default value.

View File

@ -57,6 +57,17 @@ class WC_Countries {
return $this->countries; return $this->countries;
} }
/**
* Check if a given code represents a valid ISO 3166-1 alpha-2 code for a country known to us.
*
* @since 5.1.0
* @param string $country_code The country code to check as a ISO 3166-1 alpha-2 code.
* @return bool True if the country is known to us, false otherwise.
*/
public function country_exists( $country_code ) {
return isset( $this->get_countries()[ $country_code ] );
}
/** /**
* Get all continents. * Get all continents.
* *
@ -919,7 +930,7 @@ class WC_Countries {
), ),
'state' => array( 'state' => array(
'required' => false, 'required' => false,
'hidden' => true, 'hidden' => true,
), ),
), ),
'DK' => array( 'DK' => array(
@ -928,7 +939,7 @@ class WC_Countries {
), ),
'state' => array( 'state' => array(
'required' => false, 'required' => false,
'hidden' => true, 'hidden' => true,
), ),
), ),
'EE' => array( 'EE' => array(
@ -999,7 +1010,7 @@ class WC_Countries {
), ),
), ),
'HU' => array( 'HU' => array(
'last_name' => array( 'last_name' => array(
'class' => array( 'form-row-first' ), 'class' => array( 'form-row-first' ),
'priority' => 10, 'priority' => 10,
), ),
@ -1007,20 +1018,20 @@ class WC_Countries {
'class' => array( 'form-row-last' ), 'class' => array( 'form-row-last' ),
'priority' => 20, 'priority' => 20,
), ),
'postcode' => array( 'postcode' => array(
'class' => array( 'form-row-first', 'address-field' ), 'class' => array( 'form-row-first', 'address-field' ),
'priority' => 65, 'priority' => 65,
), ),
'city' => array( 'city' => array(
'class' => array( 'form-row-last', 'address-field' ), 'class' => array( 'form-row-last', 'address-field' ),
), ),
'address_1' => array( 'address_1' => array(
'priority' => 71, 'priority' => 71,
), ),
'address_2' => array( 'address_2' => array(
'priority' => 72, 'priority' => 72,
), ),
'state' => array( 'state' => array(
'label' => __( 'County', 'woocommerce' ), 'label' => __( 'County', 'woocommerce' ),
), ),
), ),
@ -1242,7 +1253,7 @@ class WC_Countries {
'required' => true, 'required' => true,
), ),
'state' => array( 'state' => array(
'label' => __( 'District', 'woocommerce' ), 'label' => __( 'District', 'woocommerce' ),
'required' => false, 'required' => false,
), ),
), ),
@ -1314,7 +1325,7 @@ class WC_Countries {
), ),
'state' => array( 'state' => array(
'required' => false, 'required' => false,
'hidden' => true, 'hidden' => true,
), ),
), ),
'TR' => array( 'TR' => array(

View File

@ -8,5 +8,5 @@
*/ */
return array( return array(
// 'get_option' 'wc_get_shipping_method_count',
); );

View File

@ -0,0 +1,151 @@
<?php
/**
* Unit tests for the WC_Cart_Test class.
*
* @package WooCommerce\Tests\Checkout.
*/
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
/**
* Class WC_Checkout
*/
class WC_Checkout_Test extends \WC_Unit_Test_Case {
/**
* @var object The system under test.
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
// phpcs:disable Generic.CodeAnalysis, Squiz.Commenting
$this->sut = new class() extends WC_Checkout {
public function validate_posted_data( &$data, &$errors ) {
return parent::validate_posted_data( $data, $errors );
}
public function validate_checkout( &$data, &$errors ) {
return parent::validate_checkout( $data, $errors );
}
};
// phpcs:enable Generic.CodeAnalysis, Squiz.Commenting
}
/**
* @testdox 'validate_posted_data' adds errors for non-existing billing/shipping countries.
*
* @testWith [true, true]
* [false, false]
*
* @param bool $ship_to_different_address True to simulate shipping to a different address than the billing address.
* @param bool $expect_error_message_for_shipping_country True to expect an error to be generated for the shipping country.
*/
public function test_validate_posted_data_adds_error_for_non_existing_country( $ship_to_different_address, $expect_error_message_for_shipping_country ) {
$_POST = array(
'billing_country' => 'XX',
'shipping_country' => 'YY',
'ship_to_different_address' => $ship_to_different_address,
);
$data = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
add_filter(
'woocommerce_cart_needs_shipping_address',
function() {
return true;
}
);
$errors = new WP_Error();
$this->sut->validate_posted_data( $data, $errors );
$this->assertEquals( "'XX' is not a valid country code.", $errors->get_error_message( 'billing_country_validation' ) );
$this->assertEquals(
$expect_error_message_for_shipping_country ? "'YY' is not a valid country code." : '',
$errors->get_error_message( 'shipping_country_validation' )
);
}
/**
* @testdox 'validate_posted_data' doesn't add errors for existing billing/shipping countries.
*
* @testWith [true]
* [false]
*
* @param bool $ship_to_different_address True to simulate shipping to a different address than the billing address.
*/
public function test_validate_posted_data_does_not_add_error_for_existing_country( $ship_to_different_address ) {
$_POST = array(
'billing_country' => 'ES',
'shipping_country' => 'ES',
'ship_to_different_address' => $ship_to_different_address,
);
$data = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$errors = new WP_Error();
$this->sut->validate_posted_data( $data, $errors );
$this->assertEmpty( $errors->get_error_message( 'billing_country_validation' ) );
$this->assertEmpty( $errors->get_error_message( 'shipping_country_validation' ) );
}
/**
* @testdox 'validate_checkout' adds a "We don't ship to country X" error but only if the country exists.
*
* @testWith [ "XX", false ]
* [ "JP", true ]
*
* @param string $country The billing/shipping country.
* @param bool $expect_we_dont_ship_error True to expect a "We don't ship to X" error.
*/
public function test_validate_checkout_adds_we_dont_ship_error_only_if_country_exists( $country, $expect_we_dont_ship_error ) {
add_filter(
'woocommerce_countries_allowed_countries',
function() {
return array( 'ES' );
}
);
add_filter(
'woocommerce_cart_needs_shipping',
function() {
return true;
}
);
add_filter(
'wc_shipping_enabled',
function() {
return true;
}
);
FunctionsMockerHack::add_function_mocks(
array(
'wc_get_shipping_method_count' => function( $include_legacy = false, $enabled_only = false ) {
return 1;
},
)
);
$_POST = array(
'billing_country' => $country,
'shipping_country' => $country,
'ship_to_different_address' => false,
);
$data = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$errors = new WP_Error();
$this->sut->validate_checkout( $data, $errors );
$this->assertEquals(
$expect_we_dont_ship_error ? 'Unfortunately <strong>we do not ship to the JP</strong>. Please enter an alternative shipping address.' : '',
$errors->get_error_message( 'shipping' )
);
}
}