Co-authored-by: barryhughes <3594411+barryhughes@users.noreply.github.com>
This commit is contained in:
jonathansadowski 2023-07-10 21:17:27 +07:00 committed by GitHub
parent 714e50bf4c
commit 1cd947a320
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 196 additions and 5 deletions

View File

@ -171,6 +171,18 @@ class WC_Shortcode_Checkout {
}
}
// If we cannot match the order with the current user, ask that they verify their email address.
if ( self::guest_should_verify_email( $order, 'order-pay' ) ) {
wc_get_template(
'checkout/form-verify-email.php',
array(
'failed_submission' => ! empty( $_POST['email'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'verify_url' => $order->get_checkout_payment_url(),
)
);
return;
}
WC()->customer->set_props(
array(
'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null,
@ -258,6 +270,7 @@ class WC_Shortcode_Checkout {
if ( $order_id > 0 ) {
$order = wc_get_order( $order_id );
if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) {
$order = false;
}
@ -276,6 +289,36 @@ class WC_Shortcode_Checkout {
// Empty current cart.
wc_empty_cart();
// If the specified order ID was invalid, we still render the default order received page (which will simply
// state that the order was received, but will not output any other details: this makes it harder to probe for
// valid order IDs than if we state that the order ID was not recognized).
if ( ! $order ) {
wc_get_template( 'checkout/thankyou.php', array( 'order' => false ) );
return;
}
$order_customer_id = $order->get_customer_id();
// For non-guest orders, require the user to be logged in before showing this page.
if ( $order_customer_id && get_current_user_id() !== $order_customer_id ) {
wc_print_notice( esc_html__( 'Please log in to your account to view this order.', 'woocommerce' ), 'notice' );
woocommerce_login_form( array( 'redirect' => $order->get_checkout_order_received_url() ) );
return;
}
// For guest orders, request they verify their email address (unless we can identify them via the active user session).
if ( self::guest_should_verify_email( $order, 'order-received' ) ) {
wc_get_template(
'checkout/form-verify-email.php',
array(
'failed_submission' => ! empty( $_POST['email'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
'verify_url' => $order->get_checkout_order_received_url(),
)
);
return;
}
// Otherwise, display the thank you (order received) page.
wc_get_template( 'checkout/thankyou.php', array( 'order' => $order ) );
}
@ -317,4 +360,65 @@ class WC_Shortcode_Checkout {
}
}
/**
* Tries to determine if the user's email address should be verified before rendering either the 'order received' or
* 'order pay' pages. This should only be applied to guest orders.
*
* @param WC_Order $order The order for which a need for email verification is being determined.
* @param string $context The context in which email verification is being tested.
*
* @return bool
*/
private static function guest_should_verify_email( WC_Order $order, string $context ): bool {
$order_email = $order->get_billing_email();
$order_customer_id = $order->get_customer_id();
// If we do not have a billing email for the order (could happen in the order is created manually, or if the
// requirement for this has been removed from the checkout flow), email verification does not make sense.
if ( empty( $order_email ) ) {
return false;
}
// No verification step is needed if the user is logged in and is already associated with the order.
if ( $order_customer_id && get_current_user_id() === $order_customer_id ) {
return false;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! empty( $_POST ) && ! wp_verify_nonce( $_POST['check_submission'] ?? '', 'wc_verify_email' ) ) {
return true;
}
$session = wc()->session;
$session_email = '';
if ( is_a( $session, WC_Session::class ) ) {
$customer = $session->get( 'customer' );
$session_email = is_array( $customer ) && isset( $customer['email'] ) ? $customer['email'] : '';
}
$session_email_match = $session_email === $order->get_billing_email();
$supplied_email_match = isset( $_POST['email'] ) && sanitize_email( wp_unslash( $_POST['email'] ) ?? '' ) === $order->get_billing_email();
$can_view_orders = current_user_can( 'read_private_shop_orders' );
// If we cannot match the order with the current user, the user should verify their email address.
$email_verification_required = ! $session_email_match && ! $supplied_email_match && ! $can_view_orders;
/**
* Provides an opportunity to override the (potential) requirement for shoppers to verify their email address
* before we show information such as the order summary, or order payment page.
*
* Note that this hook is not always triggered, therefore it is (for example) unsuitable as a way of forcing
* email verification across all order confirmation/order payment scenarios. Instead, the filter primarily
* exists as a way to *remove* the email verification step.
*
* @since 7.9.0
*
* @param bool $email_verification_required If email verification is required.
* @param WC_Order $order The relevant order.
* @param string $context The context under which we are performing this check.
*/
return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, $context );
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Email verification page.
*
* This displays instead of the thankyou page any time that the customer cannot be identified.
*
* This template can be overridden by copying it to yourtheme/woocommerce/checkout/thankyou-verify-email.php.
*
* HOWEVER, on occasion WooCommerce will need to update template files and you (the theme developer) will need to copy
* the new files to your theme to maintain compatibility. We try to do this as little as possible, but it does happen.
* When this occurs the version of the template file will be bumped and the readme will list any important changes.
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
* @version 7.9.0
*
* @var bool $failed_submission Indicates if the last attempt to verify failed.
* @var string $verify_url The URL for the email verification form.
*/
defined( 'ABSPATH' ) || exit;
?>
<form name="checkout" method="post" class="woocommerce-form woocommerce-verify-email" action="<?php echo esc_url( $verify_url ); ?>" enctype="multipart/form-data">
<?php
wp_nonce_field( 'wc_verify_email', 'check_submission' );
if ( $failed_submission ) {
wc_print_notice( esc_html__( 'We were unable to verify the email address you provided. Please try again.', 'woocommerce' ), 'error' );
}
?>
<p>
<?php
printf(
/* translators: 1: opening login link 2: closing login link */
esc_html__( 'To view this page, you must either %1$slogin%2$s or verify the email address associated with the order.', 'woocommerce' ),
'<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '">',
'</a>'
);
?>
</p>
<p class="form-row">
<label for="email"><?php esc_html_e( 'Email address', 'woocommerce' ); ?>&nbsp;<span class="required">*</span></label>
<input type="email" class="input-text" name="email" id="email" autocomplete="email" />
</p>
<p class="form-row">
<button type="submit" class="woocommerce-button button <?php echo esc_attr( wc_wp_theme_get_element_class_name( 'button' ) ); ?>" name="verify" value="1">
<?php esc_html_e( 'Verify', 'woocommerce' ); ?>
</button>
</p>
</form>

View File

@ -245,6 +245,32 @@ test.describe( 'Checkout page', () => {
.textContent();
guestOrderId = await orderReceivedText.split( /(\s+)/ )[ 6 ].toString();
// If we simulate a new browser context by dropping all cookies, and reload the page, the shopper should be
// prompted to complete an email validation step before they can proceed.
await page.context().clearCookies();
await page.reload();
await expect( page.locator( 'form.woocommerce-verify-email p:nth-child(3)' ) ).toContainText(
/verify the email address associated with the order/
);
// Supplying an email address other than the actual order billing email address will take them back to the same
// page with an error message.
await page.fill( '#email', 'incorrect@email.address' );
await page.locator( 'form.woocommerce-verify-email button' ).click();
await expect( page.locator( 'form.woocommerce-verify-email p:nth-child(4)' ) ).toContainText(
/verify the email address associated with the order/
);
await expect( page.locator( 'ul.woocommerce-error li' ) ).toContainText(
/We were unable to verify the email address you provided/
);
// However if they supply the *correct* billing email address, they should see the order received page again.
await page.fill( '#email', guestEmail );
await page.locator( 'form.woocommerce-verify-email button' ).click();
await expect( page.locator( 'h1.entry-title' ) ).toContainText(
'Order received'
);
await page.goto( 'wp-login.php' );
await page.locator( 'input[name="log"]' ).fill( admin.username );
await page.locator( 'input[name="pwd"]' ).fill( admin.password );
@ -273,9 +299,9 @@ test.describe( 'Checkout page', () => {
} );
test( 'allows existing customer to place order', async ( { page } ) => {
await page.goto( 'wp-admin/' );
await page.locator( 'input[name="log"]' ).fill( customer.username );
await page.locator( 'input[name="pwd"]' ).fill( customer.password );
await page.goto( 'my-account/' );
await page.locator( 'input[name="username"]' ).fill( customer.username );
await page.locator( 'input[name="password"]' ).fill( customer.password );
await page.locator( 'text=Log In' ).click();
await page.waitForLoadState( 'networkidle' );
for ( let i = 1; i < 3; i++ ) {
@ -320,9 +346,17 @@ test.describe( 'Checkout page', () => {
.split( /(\s+)/ )[ 6 ]
.toString();
await page.goto( 'wp-login.php?loggedout=true' );
await page.waitForLoadState( 'networkidle' );
// Effect a log out/simulate a new browsing session by dropping all cookies.
await page.context().clearCookies();
await page.reload();
// Now we are logged out, return to the confirmation page: we should be asked to log back in.
await expect( page.locator( '.woocommerce-info' ) ).toContainText(
/Please log in to your account to view this order/
);
// Switch to admin user.
await page.goto( 'wp-login.php?loggedout=true' );
await page.locator( 'input[name="log"]' ).fill( admin.username );
await page.locator( 'input[name="pwd"]' ).fill( admin.password );
await page.locator( 'text=Log In' ).click();