Commit d50e497fede for woocommerce

commit d50e497fede7ad1a99442d80a8b268a84552845c
Author: Mike Jolley <mike.jolley@me.com>
Date:   Tue Mar 17 09:57:09 2026 +0000

    Fix StoreApi SessionHandler missing methods for session compatibility (#63606)

    * Add WC_Session_Interface and fix StoreApi SessionHandler compatibility

    Introduces a transport-agnostic WC_Session_Interface that defines the
    session handler contract. Both WC_Session_Handler and StoreApi\SessionHandler
    now implement this interface, ensuring type safety and compatibility.

    Transport-specific methods (cookie handling, session expiration) remain on
    their respective concrete classes. Callers needing cookie behavior use
    instanceof WC_Session_Handler checks.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Remove stale PHPStan baseline entries and fix param type

    The WC_Session_Interface provides return type docblocks, so PHPStan
    no longer reports missing return types for methods that implement it.
    Remove the now-stale baseline entries.

    Also fix do_action param to use $user_session_id (string) instead of
    $this->_customer_id (string|null) to match the @param docblock.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Move WC_Session_Interface include to general includes()

    The interface was only loaded in frontend_includes(), but
    WC_Session_Handler (which implements it) is instantiated in all
    contexts via initialize_session(). Move the include to the Interfaces
    section of includes() where all other interfaces are loaded.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Drop WC_Session_Interface, keep method additions on SessionHandler

    Per review feedback, the interface adds complexity without runtime
    benefit. Instead, just add the missing methods directly to the StoreApi
    SessionHandler so it honors the default session handler contract.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Update changelog entry to remove WC_Session_Interface reference

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Restore PHPStan baseline entries removed with interface

    The interface provided return type info that suppressed these errors.
    Without it, the baseline entries need to be present again.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Remove unneeded PHPStan baseline entry for StoreApi SessionHandler::save_data

    This method already has a @return void docblock so PHPStan doesn't flag it.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-wooplug-6218-storeapi-session-handler-compatibility b/plugins/woocommerce/changelog/fix-wooplug-6218-storeapi-session-handler-compatibility
new file mode 100644
index 00000000000..c23260cd8cb
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-6218-storeapi-session-handler-compatibility
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Add missing session methods to StoreApi SessionHandler for compatibility with WC_Session_Handler.
diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php
index 209ddfd2aa6..d732bcff540 100644
--- a/plugins/woocommerce/includes/class-wc-session-handler.php
+++ b/plugins/woocommerce/includes/class-wc-session-handler.php
@@ -236,7 +236,7 @@ class WC_Session_Handler extends WC_Session {
 		 * @param string $guest_session_id The former session ID, as generated by `::generate_customer_id()`.
 		 * @param string $user_session_id The Customer ID that the former session was converted to.
 		 */
-		do_action( 'woocommerce_guest_session_to_user_id', $guest_session_id, $this->_customer_id );
+		do_action( 'woocommerce_guest_session_to_user_id', $guest_session_id, $user_session_id );

 		$this->set_session_expiration();
 		$this->update_session_timestamp( $this->get_customer_id(), $this->_session_expiration );
@@ -531,7 +531,10 @@ class WC_Session_Handler extends WC_Session {
 	}

 	/**
-	 * Get session data.
+	 * Get session data fresh from storage.
+	 *
+	 * This re-reads session data from the database rather than returning
+	 * in-memory data, ensuring the latest persisted state is returned.
 	 *
 	 * @return array
 	 */
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 6076e6b4cc8..b5186936054 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -71046,18 +71046,6 @@ parameters:
 			count: 2
 			path: src/Internal/StockNotifications/Emails/EmailActionController.php

-		-
-			message: '#^Call to an undefined method WC_Session\:\:has_session\(\)\.$#'
-			identifier: method.notFound
-			count: 2
-			path: src/Internal/StockNotifications/Emails/EmailActionController.php
-
-		-
-			message: '#^Call to an undefined method WC_Session\:\:set_customer_session_cookie\(\)\.$#'
-			identifier: method.notFound
-			count: 2
-			path: src/Internal/StockNotifications/Emails/EmailActionController.php
-
 		-
 			message: '#^Cannot call method get_meta\(\) on Automattic\\WooCommerce\\Internal\\StockNotifications\\Notification\|true\.$#'
 			identifier: method.nonObject
@@ -74544,12 +74532,6 @@ parameters:
 			count: 1
 			path: src/StoreApi/SessionHandler.php

-		-
-			message: '#^Method Automattic\\WooCommerce\\StoreApi\\SessionHandler\:\:save_data\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: src/StoreApi/SessionHandler.php
-
 		-
 			message: '#^Property Automattic\\WooCommerce\\StoreApi\\SessionHandler\:\:\$token \(string\) does not accept array\|string\.$#'
 			identifier: assign.propertyType
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailActionController.php b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailActionController.php
index 992dc079c22..f336a9f6645 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailActionController.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/Emails/EmailActionController.php
@@ -81,9 +81,8 @@ class EmailActionController {
 			$notification->set_date_confirmed( time() );
 			$notification->save();

-			// We need session for notices to work.
-			if ( ! WC()->session->has_session() ) {
-				// Generate a random customer ID.
+			// We need a cookie-based session for notices to work on frontend pages.
+			if ( WC()->session instanceof \WC_Session_Handler && ! WC()->session->has_session() ) {
 				WC()->session->set_customer_session_cookie( true );
 			}

@@ -119,9 +118,8 @@ class EmailActionController {
 			$notification->set_date_cancelled( time() );
 			$notification->save();

-			// We need session for notices to work.
-			if ( ! WC()->session->has_session() ) {
-				// Generate a random customer ID.
+			// We need a cookie-based session for notices to work on frontend pages.
+			if ( WC()->session instanceof \WC_Session_Handler && ! WC()->session->has_session() ) {
 				WC()->session->set_customer_session_cookie( true );
 			}

diff --git a/plugins/woocommerce/src/StoreApi/SessionHandler.php b/plugins/woocommerce/src/StoreApi/SessionHandler.php
index 6c725a96d44..24fc73eda70 100644
--- a/plugins/woocommerce/src/StoreApi/SessionHandler.php
+++ b/plugins/woocommerce/src/StoreApi/SessionHandler.php
@@ -6,11 +6,17 @@ namespace Automattic\WooCommerce\StoreApi;
 use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\StoreApi\Utilities\CartTokenUtils;
 use WC_Session;
-
 defined( 'ABSPATH' ) || exit;

 /**
  * SessionHandler class
+ *
+ * Token-based session handler for the Store API. Unlike WC_Session_Handler which
+ * uses browser cookies, this handler uses an HTTP_CART_TOKEN header (JWT-like) to
+ * identify sessions. It shares the same database table but has no cookie, cron,
+ * or cache layer.
+ *
+ * @since 10.7.0
  */
 final class SessionHandler extends WC_Session {
 	/**
@@ -61,12 +67,54 @@ final class SessionHandler extends WC_Session {
 		$this->_data              = (array) $this->get_session( $this->get_customer_id(), array() );
 	}

+	/**
+	 * Return true if the current user has an active session.
+	 *
+	 * @return bool
+	 */
+	public function has_session() {
+		return ! empty( $this->_customer_id );
+	}
+
+	/**
+	 * Generate a unique customer ID for guests, or return user ID if logged in.
+	 *
+	 * @return string
+	 */
+	public function generate_customer_id() {
+		return is_user_logged_in() ? (string) get_current_user_id() : wc_rand_hash( 't_', 30 );
+	}
+
+	/**
+	 * Get session unique ID for requests if session is initialized or user ID if logged in.
+	 *
+	 * @return string
+	 */
+	public function get_customer_unique_id() {
+		if ( $this->has_session() && $this->get_customer_id() ) {
+			return $this->get_customer_id();
+		}
+		return is_user_logged_in() ? (string) get_current_user_id() : '';
+	}
+
+	/**
+	 * Get session data fresh from storage.
+	 *
+	 * This re-reads session data from the database rather than returning
+	 * in-memory data, ensuring the latest persisted state is returned.
+	 *
+	 * @return array
+	 */
+	public function get_session_data() {
+		return $this->has_session() ? (array) $this->get_session( $this->get_customer_id(), array() ) : array();
+	}
+
 	/**
 	 * Returns the session.
 	 *
 	 * @param string $customer_id Customer ID.
 	 * @param mixed  $default_value Default session value.
-
+	 *
 	 * @return mixed Returns either the session data or the default value. Returns false if WP setup is in progress.
 	 */
 	public function get_session( $customer_id, $default_value = false ) {
@@ -92,8 +140,44 @@ final class SessionHandler extends WC_Session {
 		return maybe_unserialize( $value );
 	}

+	/**
+	 * Destroy all session data.
+	 *
+	 * @return void
+	 */
+	public function destroy_session() {
+		$this->delete_session( $this->get_customer_id() );
+		$this->forget_session();
+	}
+
+	/**
+	 * Forget all session data without destroying persisted storage.
+	 *
+	 * @return void
+	 */
+	public function forget_session() {
+		$this->_data        = array();
+		$this->_dirty       = false;
+		$this->_customer_id = null;
+	}
+
+	/**
+	 * Delete the session from the database.
+	 *
+	 * @param string $customer_id Customer session ID.
+	 * @return void
+	 */
+	public function delete_session( $customer_id ) {
+		if ( ! $customer_id ) {
+			return;
+		}
+		$GLOBALS['wpdb']->delete( $this->table, array( 'session_key' => $customer_id ) );
+	}
+
 	/**
 	 * Save data and delete user session.
+	 *
+	 * @return void
 	 */
 	public function save_data() {
 		// Dirty if something changed - prevents saving nothing new.
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/SessionHandlerTest.php b/plugins/woocommerce/tests/php/src/StoreApi/SessionHandlerTest.php
new file mode 100644
index 00000000000..291b90c9a09
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/StoreApi/SessionHandlerTest.php
@@ -0,0 +1,123 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\StoreApi;
+
+use Automattic\WooCommerce\StoreApi\SessionHandler;
+use WC_Session;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the StoreApi SessionHandler class.
+ */
+class SessionHandlerTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var SessionHandler
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$_SERVER['HTTP_CART_TOKEN'] = '';
+
+		$this->sut = new SessionHandler();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		unset( $_SERVER['HTTP_CART_TOKEN'] );
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox SessionHandler extends WC_Session.
+	 */
+	public function test_extends_wc_session(): void {
+		$this->assertInstanceOf( WC_Session::class, $this->sut, 'SessionHandler should extend WC_Session' );
+	}
+
+	/**
+	 * @testdox has_session returns false when no customer ID is set.
+	 */
+	public function test_has_session_returns_false_without_customer_id(): void {
+		$this->assertFalse( $this->sut->has_session(), 'Should return false when no customer ID is set' );
+	}
+
+	/**
+	 * @testdox has_session returns true when a customer ID is set.
+	 */
+	public function test_has_session_returns_true_with_customer_id(): void {
+		$reflection = new \ReflectionProperty( $this->sut, '_customer_id' );
+		$reflection->setAccessible( true );
+		$reflection->setValue( $this->sut, 'test_customer_123' );
+
+		$this->assertTrue( $this->sut->has_session(), 'Should return true when customer ID is set' );
+	}
+
+	/**
+	 * @testdox generate_customer_id returns a non-empty string.
+	 */
+	public function test_generate_customer_id_returns_string(): void {
+		$result = $this->sut->generate_customer_id();
+		$this->assertNotEmpty( $result, 'generate_customer_id should return a non-empty string' );
+		$this->assertIsString( $result, 'generate_customer_id should return a string' );
+	}
+
+	/**
+	 * @testdox get_customer_unique_id returns empty string when no session.
+	 */
+	public function test_get_customer_unique_id_returns_empty_without_session(): void {
+		wp_set_current_user( 0 );
+		$this->assertSame( '', $this->sut->get_customer_unique_id(), 'Should return empty string when no session and not logged in' );
+	}
+
+	/**
+	 * @testdox forget_session clears data and customer ID.
+	 */
+	public function test_forget_session_clears_state(): void {
+		$reflection = new \ReflectionProperty( $this->sut, '_customer_id' );
+		$reflection->setAccessible( true );
+		$reflection->setValue( $this->sut, 'test_customer_123' );
+
+		$this->sut->set( 'test_key', 'test_value' );
+
+		$this->sut->forget_session();
+
+		$this->assertSame( '', $this->sut->get_customer_id(), 'Customer ID should be cleared after forget_session' );
+		$this->assertNull( $this->sut->get( 'test_key' ), 'Session data should be cleared after forget_session' );
+	}
+
+	/**
+	 * @testdox destroy_session clears data and session state.
+	 */
+	public function test_destroy_session_clears_state(): void {
+		$reflection = new \ReflectionProperty( $this->sut, '_customer_id' );
+		$reflection->setAccessible( true );
+		$reflection->setValue( $this->sut, 'test_customer_123' );
+
+		$this->sut->set( 'test_key', 'test_value' );
+
+		$this->sut->destroy_session();
+
+		$this->assertNull( $this->sut->get( 'test_key' ), 'Session data should be cleared after destroy_session' );
+		$this->assertFalse( $this->sut->has_session(), 'has_session should return false after destroy_session' );
+		$this->assertSame( '', $this->sut->get_customer_id(), 'Customer ID should be cleared after destroy_session' );
+	}
+
+	/**
+	 * @testdox set and get work for session data.
+	 */
+	public function test_set_and_get_session_data(): void {
+		$this->sut->set( 'test_key', 'test_value' );
+		$this->assertSame( 'test_value', $this->sut->get( 'test_key' ), 'Should return the value that was set' );
+	}
+}