Commit d28b900ecf for woocommerce

commit d28b900ecf82c91fde8728e7645415141bb32d99
Author: Eric Binnion <ericbinnion@gmail.com>
Date:   Thu Feb 5 21:02:16 2026 -0500

    Store API: Address bug where non wp-json did not respect cart-token (#62973)

    * Store API: Address bug where non wp-json did not respect cart-token

    Co-authored-by: Radoslav Georgiev <rageorgiev@gmail.com>

diff --git a/plugins/woocommerce/changelog/62973-fix-store-api-cart-token-rest-route b/plugins/woocommerce/changelog/62973-fix-store-api-cart-token-rest-route
new file mode 100644
index 0000000000..855dad32da
--- /dev/null
+++ b/plugins/woocommerce/changelog/62973-fix-store-api-cart-token-rest-route
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Ensure Store API cart sessions restore correctly for rest_route requests with Cart-Token.
diff --git a/plugins/woocommerce/src/StoreApi/Authentication.php b/plugins/woocommerce/src/StoreApi/Authentication.php
index 1204650a8d..f70ab87af7 100644
--- a/plugins/woocommerce/src/StoreApi/Authentication.php
+++ b/plugins/woocommerce/src/StoreApi/Authentication.php
@@ -40,6 +40,26 @@ class Authentication {
 		return $allowed_headers;
 	}

+	/**
+	 * Use the Store API session handler when a valid Cart-Token is present.
+	 *
+	 * @since 10.6.0
+	 * @param string $handler Session handler class name.
+	 * @return string
+	 */
+	public function maybe_use_store_api_session_handler( $handler ): string {
+		if ( ! WC()->is_store_api_request() && ! $this->has_store_api_route_as_get_parameter() ) {
+			return $handler;
+		}
+
+		$cart_token = wc_clean( wp_unslash( $_SERVER['HTTP_CART_TOKEN'] ?? '' ) );
+		$cart_token = is_string( $cart_token ) ? $cart_token : '';
+		if ( $cart_token && CartTokenUtils::validate_cart_token( $cart_token ) ) {
+			return SessionHandler::class;
+		}
+		return $handler;
+	}
+
 	/**
 	 * Expose Store API headers in CORS responses.
 	 * We're explicitly exposing the Cart-Token, not the nonce. Only one of them is needed.
@@ -96,6 +116,23 @@ class Authentication {
 		return $served;
 	}

+	/**
+	 * Checks if the request has a store API route as a GET `rest_route` parameter.
+	 *
+	 * @since 10.6.0
+	 * @return bool
+	 */
+	protected function has_store_api_route_as_get_parameter(): bool {
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Store API
+		if ( ! isset( $_GET['rest_route'] ) || ! is_string( $_GET['rest_route'] ) ) {
+			return false;
+		}
+
+		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Store API context check.
+		$rest_route = rawurldecode( esc_url_raw( wp_unslash( $_GET['rest_route'] ) ) );
+		return 0 === strpos( $rest_route, '/wc/store/' );
+	}
+
 	/**
 	 * Is the request a preflight request? Checks the request method
 	 *
diff --git a/plugins/woocommerce/src/StoreApi/StoreApi.php b/plugins/woocommerce/src/StoreApi/StoreApi.php
index dccea2dd87..50a9572b9b 100644
--- a/plugins/woocommerce/src/StoreApi/StoreApi.php
+++ b/plugins/woocommerce/src/StoreApi/StoreApi.php
@@ -20,6 +20,15 @@ final class StoreApi {
 	 * Init and hook in Store API functionality.
 	 */
 	public function init() {
+		/**
+		 * Authentication instance.
+		 *
+		 * @var Authentication $authentication
+		 */
+		$authentication = self::container()->get( Authentication::class );
+
+		add_filter( 'woocommerce_session_handler', array( $authentication, 'maybe_use_store_api_session_handler' ), 0 );
+
 		add_action(
 			'rest_api_init',
 			function () {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
index 47a75ee757..b3b4c295f1 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Cart.php
@@ -7,7 +7,9 @@ namespace Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes;

 use Automattic\WooCommerce\Tests\Blocks\Helpers\FixtureData;
 use Automattic\WooCommerce\Tests\Blocks\Helpers\ValidateSchema;
+use Automattic\WooCommerce\StoreApi\Authentication;
 use Automattic\WooCommerce\StoreApi\SessionHandler;
+use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
 use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
 use Spy_REST_Server;
 use Automattic\WooCommerce\Enums\ProductStockStatus;
@@ -613,6 +615,84 @@ class Cart extends ControllerTestCase {
 		);
 	}

+	/**
+	 * Test Store API uses SessionHandler when Cart-Token is present via REQUEST_URI.
+	 */
+	public function test_store_api_uses_session_handler_for_cart_token() {
+		$customer_id = (string) wc()->session->get_customer_id();
+		$token       = CartTokenUtils::get_cart_token( $customer_id );
+
+		// Preserve globals.
+		$old_server = $_SERVER;
+
+		try {
+			// Simulate a Store API request with valid Cart-Token.
+			$_SERVER['REQUEST_URI']     = '/' . rest_get_url_prefix() . '/wc/store/v1/cart';
+			$_SERVER['HTTP_CART_TOKEN'] = $token;
+
+			$authentication = new Authentication();
+			$result         = $authentication->maybe_use_store_api_session_handler( 'WC_Session_Handler' );
+
+			$this->assertSame( SessionHandler::class, $result );
+		} finally {
+			// Restore globals.
+			$_SERVER = $old_server;
+		}
+	}
+
+	/**
+	 * Test Store API uses SessionHandler when rest_route GET parameter is present.
+	 */
+	public function test_rest_route_get_parameter_uses_store_api_session_handler() {
+		$customer_id = (string) wc()->session->get_customer_id();
+		$token       = CartTokenUtils::get_cart_token( $customer_id );
+
+		// Preserve globals.
+		$old_get    = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Test context.
+		$old_server = $_SERVER;
+
+		try {
+			// Simulate a Store API request via GET parameter with valid Cart-Token.
+			$_GET['rest_route']         = '/wc/store/v1/cart';
+			$_SERVER['HTTP_CART_TOKEN'] = $token;
+
+			$authentication = new Authentication();
+			$result         = $authentication->maybe_use_store_api_session_handler( 'WC_Session_Handler' );
+
+			$this->assertSame( SessionHandler::class, $result );
+		} finally {
+			// Restore globals.
+			$_GET    = $old_get;
+			$_SERVER = $old_server;
+		}
+	}
+
+	/**
+	 * Test non-Store API routes do not switch to Store API session handler.
+	 */
+	public function test_non_store_api_route_does_not_use_store_api_session_handler() {
+		$customer_id = (string) wc()->session->get_customer_id();
+		$token       = CartTokenUtils::get_cart_token( $customer_id );
+
+		// Preserve globals.
+		$old_server = $_SERVER;
+
+		try {
+			// Simulate a non-Store API request (even with valid Cart-Token).
+			$_SERVER['REQUEST_URI']     = '/' . rest_get_url_prefix() . '/wp/v2/posts';
+			$_SERVER['HTTP_CART_TOKEN'] = $token;
+
+			$authentication = new Authentication();
+			$result         = $authentication->maybe_use_store_api_session_handler( 'WC_Session_Handler' );
+
+			// Should return the default handler for non-Store API routes.
+			$this->assertSame( 'WC_Session_Handler', $result );
+		} finally {
+			// Restore globals.
+			$_SERVER = $old_server;
+		}
+	}
+
 	/**
 	 * Test that cart GET endpoint sends Cache-Control headers.
 	 */