diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index 076b8a48e32..9dfa2474b42 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -1134,9 +1134,19 @@ class WC_Checkout {
*/
public function process_checkout() {
try {
- $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore
+ $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore
+ $expiry_message = sprintf(
+ /* translators: %s: shop cart url */
+ __( 'Sorry, your session has expired. Return to shop', 'woocommerce' ),
+ esc_url( wc_get_page_permalink( 'shop' ) )
+ );
if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) {
+ // If the cart is empty, the nonce check failed because of session expiry.
+ if ( WC()->cart->is_empty() ) {
+ throw new Exception( $expiry_message );
+ }
+
WC()->session->set( 'refresh_totals', true );
throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) );
}
@@ -1147,8 +1157,7 @@ class WC_Checkout {
do_action( 'woocommerce_before_checkout_process' );
if ( WC()->cart->is_empty() ) {
- /* translators: %s: shop cart url */
- throw new Exception( sprintf( __( 'Sorry, your session has expired. Return to shop', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) );
+ throw new Exception( $expiry_message );
}
do_action( 'woocommerce_checkout_process' );
diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php
index 0c6dda12484..a98bf04bc78 100644
--- a/plugins/woocommerce/includes/class-wc-session-handler.php
+++ b/plugins/woocommerce/includes/class-wc-session-handler.php
@@ -88,12 +88,18 @@ class WC_Session_Handler extends WC_Session {
$cookie = $this->get_session_cookie();
if ( $cookie ) {
+ // Customer ID will be an MD5 hash id this is a guest session.
$this->_customer_id = $cookie[0];
$this->_session_expiration = $cookie[1];
$this->_session_expiring = $cookie[2];
$this->_has_cookie = true;
$this->_data = $this->get_session_data();
+ if ( ! $this->is_session_cookie_valid() ) {
+ $this->destroy_session();
+ $this->set_session_expiration();
+ }
+
// If the user logs in, update session.
if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) {
$guest_session_id = $this->_customer_id;
@@ -115,6 +121,30 @@ class WC_Session_Handler extends WC_Session {
}
}
+ /**
+ * Checks if session cookie is expired, or belongs to a logged out user.
+ *
+ * @return bool Whether session cookie is valid.
+ */
+ private function is_session_cookie_valid() {
+ // If session is expired, session cookie is invalid.
+ if ( time() > $this->_session_expiration ) {
+ return false;
+ }
+
+ // If user has logged out, session cookie is invalid.
+ if ( ! is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) ) {
+ return false;
+ }
+
+ // Session from a different user is not valid. (Although from a guest user will be valid)
+ if ( is_user_logged_in() && ! $this->is_customer_guest( $this->_customer_id ) && strval( get_current_user_id() ) !== $this->_customer_id ) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Sets the session cookie on-demand (usually after adding an item to the cart).
*
@@ -181,12 +211,54 @@ class WC_Session_Handler extends WC_Session {
if ( empty( $customer_id ) ) {
require_once ABSPATH . 'wp-includes/class-phpass.php';
$hasher = new PasswordHash( 8, false );
- $customer_id = md5( $hasher->get_random_bytes( 32 ) );
+ $customer_id = 't_' . substr( md5( $hasher->get_random_bytes( 32 ) ), 2 );
}
return $customer_id;
}
+ /**
+ * Checks if this is an auto-generated customer ID.
+ *
+ * @param string|int $customer_id Customer ID to check.
+ *
+ * @return bool Whether customer ID is randomly generated.
+ */
+ private function is_customer_guest( $customer_id ) {
+ $customer_id = strval( $customer_id );
+
+ if ( empty( $customer_id ) ) {
+ return true;
+ }
+
+ if ( 't_' === substr( $customer_id, 0, 2 ) ) {
+ return true;
+ }
+
+ /**
+ * Legacy checks. This is to handle sessions that were created from a previous release.
+ * Maybe we can get rid of them after a few releases.
+ */
+
+ // Almost all random $customer_ids will have some letters in it, while all actual ids will be integers.
+ if ( strval( (int) $customer_id ) !== $customer_id ) {
+ return true;
+ }
+
+ // Performance hack to potentially save a DB query, when same user as $customer_id is logged in.
+ if ( is_user_logged_in() && strval( get_current_user_id() ) === $customer_id ) {
+ return false;
+ } else {
+ $customer = new WC_Customer( $customer_id );
+
+ if ( 0 === $customer->get_id() ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Get session unique ID for requests if session is initialized or user ID if logged in.
* Introduced to help with unit tests.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php b/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
index 7af97b30165..bb60510722d 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/session/class-wc-tests-session-handler.php
@@ -130,12 +130,111 @@ class WC_Tests_Session_Handler extends WC_Unit_Test_Case {
$this->assertEquals( $this->handler->get_customer_unique_id(), $this->handler->maybe_update_nonce_user_logged_out( 1, 'woocommerce-something' ) );
}
+ /**
+ * @testdox Test that session from cookie is destroyed if expired.
+ */
+ public function test_destroy_session_cookie_expired() {
+ $customer_id = '1';
+ $session_expiration = time() - 10000;
+ $session_expiring = time() - 1000;
+ $cookie_hash = '';
+ $this->session_key = $customer_id;
+
+ $handler = $this
+ ->getMockBuilder( WC_Session_Handler::class )
+ ->setMethods( array( 'get_session_cookie' ) )
+ ->getMock();
+
+ $handler
+ ->method( 'get_session_cookie' )
+ ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) );
+
+ add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $handler->init_session_cookie();
+
+ remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $this->assertFalse( wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) );
+ $this->assertNull( $this->get_session_from_db( $this->session_key ) );
+ }
+
+ /**
+ * @testdox Test that session from cookie is destroyed if user is logged out.
+ */
+ public function test_destroy_session_user_logged_out() {
+ $customer_id = '1';
+ $session_expiration = time() + 50000;
+ $session_expiring = time() + 5000;
+ $cookie_hash = '';
+ $this->session_key = $customer_id;
+
+ // Simulate a log out.
+ wp_set_current_user( 0 );
+
+ $handler = $this
+ ->getMockBuilder( WC_Session_Handler::class )
+ ->setMethods( array( 'get_session_cookie' ) )
+ ->getMock();
+
+ $handler
+ ->method( 'get_session_cookie' )
+ ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) );
+
+ add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $handler->init_session_cookie();
+
+ remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $this->assertFalse( wp_cache_get( $this->cache_prefix . $this->session_key, WC_SESSION_CACHE_GROUP ) );
+ $this->assertNull( $this->get_session_from_db( $this->session_key ) );
+ }
+
+ /**
+ * @testdox Test that session from cookie is destroyed if logged in user doesn't match.
+ */
+ public function test_destroy_session_user_mismatch() {
+ $customer = WC_Helper_Customer::create_customer();
+ $customer_id = (string) $customer->get_id();
+ $session_expiration = time() + 50000;
+ $session_expiring = time() + 5000;
+ $cookie_hash = '';
+
+ $handler = $this
+ ->getMockBuilder( WC_Session_Handler::class )
+ ->setMethods( array( 'get_session_cookie' ) )
+ ->getMock();
+
+ wp_set_current_user( $customer->get_id() );
+
+ $handler->init();
+ $handler->set( 'cart', 'fake cart' );
+ $handler->save_data();
+
+ $handler
+ ->method( 'get_session_cookie' )
+ ->willReturn( array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) );
+
+ wp_set_current_user( 1 );
+
+ add_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $handler->init_session_cookie();
+
+ remove_filter( 'woocommerce_set_cookie_enabled', '__return_false' );
+
+ $this->assertFalse( wp_cache_get( $this->cache_prefix . $customer_id, WC_SESSION_CACHE_GROUP ) );
+ $this->assertNull( $this->get_session_from_db( $customer_id ) );
+ $this->assertNotNull( $this->get_session_from_db( '1' ) );
+ }
+
/**
* Helper function to create a WC session and save it to the DB.
*/
protected function create_session() {
- $this->handler->init();
wp_set_current_user( 1 );
+ $this->handler->init();
$this->handler->set( 'cart', 'fake cart' );
$this->handler->save_data();
$this->session_key = $this->handler->get_customer_id();