Commit 01c919014e8 for woocommerce
commit 01c919014e848ee5453cbd3b24f07211cfbeb24b
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Wed May 13 13:59:11 2026 -0400
Fix invalid WooCommerce cache prefixes (#64808)
* Fix invalid WooCommerce cache prefixes
* Fix cache prefix PHPStan failure
* Simplify cache prefix regeneration
* Relax cached prefix validation
* Remove stale cache prefix PHPStan baselines
* Add invalid cache prefix detection hook
* Simplify cache prefix recovery
* Remove redundant cache prefix found cast
* Document concurrent cache prefix initialization
diff --git a/plugins/woocommerce/changelog/fix-cache-prefix-invalid-values b/plugins/woocommerce/changelog/fix-cache-prefix-invalid-values
new file mode 100644
index 00000000000..219872b8de2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-cache-prefix-invalid-values
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent invalid cache prefixes from causing fatal errors on the My Account orders page.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 9fbecb56f3e..e4bc5e40ff0 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -9702,12 +9702,6 @@ parameters:
count: 1
path: includes/class-wc-cache-helper.php
- -
- message: '#^Method WC_Cache_Helper\:\:invalidate_cache_group\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: includes/class-wc-cache-helper.php
-
-
message: '#^Method WC_Cache_Helper\:\:notices\(\) has no return type specified\.$#'
identifier: missingType.return
@@ -56661,12 +56655,6 @@ parameters:
count: 1
path: src/Caching/WPCacheEngine.php
- -
- message: '#^Method Automattic\\WooCommerce\\Caching\\WPCacheEngine\:\:invalidate_cache_group\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Caching/WPCacheEngine.php
-
-
message: '#^Parameter \#1 \$data of function wp_cache_set_multiple expects array, array\<string, mixed\>\|false given\.$#'
identifier: argument.type
diff --git a/plugins/woocommerce/src/Caching/CacheNameSpaceTrait.php b/plugins/woocommerce/src/Caching/CacheNameSpaceTrait.php
index 7c4f4cbb8c2..18f364249dd 100644
--- a/plugins/woocommerce/src/Caching/CacheNameSpaceTrait.php
+++ b/plugins/woocommerce/src/Caching/CacheNameSpaceTrait.php
@@ -22,13 +22,44 @@ trait CacheNameSpaceTrait {
*/
public static function get_cache_prefix( $group ) {
// Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed.
- $prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group );
+ $cache_key = 'wc_' . $group . '_cache_prefix';
+ $found = false;
+ $prefix = wp_cache_get( $cache_key, $group, false, $found );
- if ( false === $prefix ) {
- $prefix = microtime();
- wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group );
+ if ( self::is_valid_cache_prefix( $prefix ) ) {
+ return 'wc_cache_' . $prefix . '_';
}
+ if ( $found ) {
+ /**
+ * Fires when WooCommerce detects an invalid cache prefix before replacing it.
+ *
+ * @since 10.8.0
+ *
+ * @param string $group Cache group.
+ * @param mixed $prefix Invalid cached prefix value.
+ */
+ do_action( 'woocommerce_invalid_cache_prefix_detected', $group, $prefix );
+ }
+
+ $prefix = self::generate_cache_prefix();
+
+ if ( ! $found ) {
+ // Use add on cold prefixes so concurrent requests converge on the first
+ // persisted value instead of writing competing cache namespaces.
+ if ( wp_cache_add( $cache_key, $prefix, $group ) ) {
+ return 'wc_cache_' . $prefix . '_';
+ }
+
+ $cached_prefix = wp_cache_get( $cache_key, $group );
+
+ if ( self::is_valid_cache_prefix( $cached_prefix ) ) {
+ return 'wc_cache_' . $cached_prefix . '_';
+ }
+ }
+
+ wp_cache_set( $cache_key, $prefix, $group );
+
return 'wc_cache_' . $prefix . '_';
}
@@ -46,10 +77,12 @@ trait CacheNameSpaceTrait {
* Invalidate cache group.
*
* @param string $group Group of cache to clear.
+ * @return bool True when the new prefix was persisted to the object cache,
+ * false otherwise.
* @since 3.9.0
*/
public static function invalidate_cache_group( $group ) {
- return wp_cache_set( 'wc_' . $group . '_cache_prefix', microtime(), $group );
+ return wp_cache_set( 'wc_' . $group . '_cache_prefix', self::generate_cache_prefix(), $group );
}
/**
@@ -63,4 +96,23 @@ trait CacheNameSpaceTrait {
public static function get_prefixed_key( $key, $group ) {
return self::get_cache_prefix( $group ) . $key;
}
+
+ /**
+ * Generate a cache-safe prefix value.
+ *
+ * @return string Cache prefix.
+ */
+ private static function generate_cache_prefix() {
+ return str_replace( ' ', '_', microtime() ) . '_' . bin2hex( random_bytes( 8 ) );
+ }
+
+ /**
+ * Check whether a cached prefix can be used as a cache-key namespace.
+ *
+ * @param mixed $prefix Cached prefix value.
+ * @return bool True if the prefix is valid.
+ */
+ private static function is_valid_cache_prefix( $prefix ) {
+ return is_string( $prefix ) && '' !== trim( $prefix );
+ }
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cache-helper-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cache-helper-test.php
index f1923887cda..80dcbc90f50 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cache-helper-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cache-helper-test.php
@@ -7,6 +7,24 @@ declare( strict_types = 1 );
*/
class WC_Cache_Helper_Tests extends WC_Unit_Test_Case {
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->delete_orders_cache_prefixes();
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ $this->delete_orders_cache_prefixes();
+
+ parent::tearDown();
+ }
+
/**
* Data provider for test_geolocation_ajax_get_location_hash.
*
@@ -74,4 +92,189 @@ class WC_Cache_Helper_Tests extends WC_Unit_Test_Case {
WC_Cache_Helper::geolocation_ajax_get_location_hash()
);
}
+
+ /**
+ * @testdox Get cache prefix should generate string prefixes for empty cache groups.
+ */
+ public function test_get_cache_prefix_generates_string_prefix(): void {
+ $prefix = WC_Cache_Helper::get_cache_prefix( 'orders' );
+
+ $this->assert_prefixed_cache_key_namespace( $prefix );
+ }
+
+ /**
+ * @testdox Get cache prefix should store string prefixes.
+ */
+ public function test_get_cache_prefix_stores_string_prefix(): void {
+ WC_Cache_Helper::get_cache_prefix( 'orders' );
+
+ $stored_prefix = wp_cache_get( 'wc_orders_cache_prefix', 'orders' );
+
+ $this->assert_valid_stored_prefix( $stored_prefix );
+ }
+
+ /**
+ * Data provider for invalid cache prefix values.
+ *
+ * @return array<string,array{0:mixed}>
+ */
+ public function data_provider_invalid_cache_prefixes(): array {
+ return array(
+ 'null' => array( null ),
+ 'array' => array( array( 'invalid' => true ) ),
+ 'true' => array( true ),
+ 'false' => array( false ),
+ 'integer' => array( 123 ),
+ 'float' => array( 123.45 ),
+ 'stdClass' => array( (object) array( 'invalid' => true ) ),
+ 'stringable object' => array(
+ new class() {
+ /**
+ * Convert the object to a string.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return '0.12345600_1778592656_deadbeefdeadbeef';
+ }
+ },
+ ),
+ 'empty string' => array( '' ),
+ 'spaces' => array( ' ' ),
+ 'tab' => array( "\t" ),
+ 'newline' => array( "\n" ),
+ 'null byte' => array( "\0" ),
+ );
+ }
+
+ /**
+ * @testdox Get cache prefix should replace invalid cached values.
+ *
+ * @dataProvider data_provider_invalid_cache_prefixes
+ *
+ * @param mixed $invalid_prefix Invalid cache prefix.
+ */
+ public function test_get_cache_prefix_replaces_invalid_cached_prefixes( $invalid_prefix ): void {
+ wp_cache_set( 'wc_orders_cache_prefix', $invalid_prefix, 'orders' );
+
+ $prefix = WC_Cache_Helper::get_cache_prefix( 'orders' );
+ $stored_prefix = wp_cache_get( 'wc_orders_cache_prefix', 'orders' );
+
+ $this->assert_prefixed_cache_key_namespace( $prefix );
+ $this->assert_valid_stored_prefix( $stored_prefix );
+ $this->assertNotSame( $invalid_prefix, $stored_prefix );
+ }
+
+ /**
+ * @testdox Get cache prefix should fire an action when replacing an invalid cached prefix.
+ */
+ public function test_get_cache_prefix_fires_action_when_replacing_invalid_cached_prefix(): void {
+ $invalid_prefix = array( 'invalid' => true );
+ $detected = array();
+ $callback = function ( $group, $prefix ) use ( &$detected ) {
+ $detected[] = array(
+ 'group' => $group,
+ 'prefix' => $prefix,
+ );
+ };
+
+ wp_cache_set( 'wc_orders_cache_prefix', $invalid_prefix, 'orders' );
+ add_action( 'woocommerce_invalid_cache_prefix_detected', $callback, 10, 2 );
+
+ try {
+ WC_Cache_Helper::get_cache_prefix( 'orders' );
+ } finally {
+ remove_action( 'woocommerce_invalid_cache_prefix_detected', $callback, 10 );
+ }
+
+ $this->assertCount( 1, $detected );
+ $this->assertSame( 'orders', $detected[0]['group'] );
+ $this->assertSame( $invalid_prefix, $detected[0]['prefix'] );
+ }
+
+ /**
+ * Data provider for valid cache prefix values.
+ *
+ * @return array<string,array{0:string}>
+ */
+ public function data_provider_valid_cache_prefixes(): array {
+ return array(
+ 'generated format' => array( '0.12345600_1778592656_deadbeefdeadbeef' ),
+ 'old microtime' => array( '0.84069400 1778478731' ),
+ 'plain string' => array( 'extension-prefix' ),
+ 'string with spaces' => array( 'extension prefix' ),
+ 'numeric string zero' => array( '0' ),
+ );
+ }
+
+ /**
+ * @testdox Get cache prefix should reuse valid cached values.
+ *
+ * @dataProvider data_provider_valid_cache_prefixes
+ *
+ * @param string $stored_prefix Stored cache prefix.
+ */
+ public function test_get_cache_prefix_reuses_valid_cached_prefix( string $stored_prefix ): void {
+ wp_cache_set( 'wc_orders_cache_prefix', $stored_prefix, 'orders' );
+
+ $prefix = WC_Cache_Helper::get_cache_prefix( 'orders' );
+
+ $this->assertSame( 'wc_cache_' . $stored_prefix . '_', $prefix );
+ $this->assertSame( $stored_prefix, wp_cache_get( 'wc_orders_cache_prefix', 'orders' ) );
+ }
+
+ /**
+ * @testdox Invalidate cache group should generate string prefixes.
+ */
+ public function test_invalidate_cache_group_generates_string_prefix(): void {
+ WC_Cache_Helper::invalidate_cache_group( 'orders' );
+
+ $prefix = WC_Cache_Helper::get_cache_prefix( 'orders' );
+ $stored_prefix = wp_cache_get( 'wc_orders_cache_prefix', 'orders' );
+
+ $this->assert_prefixed_cache_key_namespace( $prefix );
+ $this->assert_valid_stored_prefix( $stored_prefix );
+ }
+
+ /**
+ * @testdox Get cache prefix should recover when a cached prefix is not stringable.
+ */
+ public function test_get_cache_prefix_recovers_non_stringable_cached_prefix(): void {
+ wp_cache_set( 'wc_orders_cache_prefix', (object) array( 'invalid' => true ), 'orders' );
+
+ $cache_key = WC_Order::generate_meta_cache_key( 123, 'orders' );
+ $stored_prefix = wp_cache_get( 'wc_orders_cache_prefix', 'orders' );
+
+ $this->assertStringStartsWith( 'wc_cache_', $cache_key );
+ $this->assertStringContainsString( 'object_meta_123', $cache_key );
+ $this->assert_valid_stored_prefix( $stored_prefix );
+ }
+
+ /**
+ * Assert that a namespaced cache key contains a valid prefix.
+ *
+ * @param string $prefix Namespaced cache key prefix.
+ */
+ private function assert_prefixed_cache_key_namespace( string $prefix ): void {
+ $this->assertStringStartsWith( 'wc_cache_', $prefix );
+ $this->assertStringEndsWith( '_', $prefix );
+ $this->assert_valid_stored_prefix( substr( $prefix, strlen( 'wc_cache_' ), -1 ) );
+ }
+
+ /**
+ * Assert that a stored cache prefix can be used as a cache-key namespace.
+ *
+ * @param mixed $prefix Stored cache prefix.
+ */
+ private function assert_valid_stored_prefix( $prefix ): void {
+ $this->assertIsString( $prefix );
+ $this->assertNotSame( '', trim( $prefix ) );
+ }
+
+ /**
+ * Delete cache prefix fixtures.
+ */
+ private function delete_orders_cache_prefixes(): void {
+ wp_cache_delete( 'wc_orders_cache_prefix', 'orders' );
+ }
}