Commit 3f17c5be52 for woocommerce
commit 3f17c5be52901ee0a22ba4a6cb81f0e5d2e219e9
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Fri Feb 13 09:55:43 2026 +0100
Make VersionStringGenerator and RestApiCache more robust against bad-behaved object caches (#63259)
diff --git a/plugins/woocommerce/changelog/pr-63259 b/plugins/woocommerce/changelog/pr-63259
new file mode 100644
index 0000000000..343cfeaa54
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-63259
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Make VersionStringGenerator and RestApiCache more robust against bad-behaved object caches
diff --git a/plugins/woocommerce/src/Internal/Caches/VersionStringGenerator.php b/plugins/woocommerce/src/Internal/Caches/VersionStringGenerator.php
index e18df65b92..8aff3f0360 100644
--- a/plugins/woocommerce/src/Internal/Caches/VersionStringGenerator.php
+++ b/plugins/woocommerce/src/Internal/Caches/VersionStringGenerator.php
@@ -82,10 +82,9 @@ class VersionStringGenerator {
$this->validate_input( $id );
$cache_key = $this->get_cache_key( $id );
- $found = false;
- $version = wp_cache_get( $cache_key, self::CACHE_GROUP, false, $found );
+ $version = wp_cache_get( $cache_key, self::CACHE_GROUP );
- if ( ! $found ) {
+ if ( false === $version ) {
if ( ! $generate ) {
return null;
}
@@ -136,7 +135,24 @@ class VersionStringGenerator {
$ttl = apply_filters( 'woocommerce_version_string_generator_ttl', DAY_IN_SECONDS, $id );
$ttl = max( 0, (int) $ttl );
- return wp_cache_set( $cache_key, $version, self::CACHE_GROUP, $ttl );
+ $result = wp_cache_set( $cache_key, $version, self::CACHE_GROUP, $ttl );
+
+ if ( is_bool( $result ) ) {
+ return $result;
+ }
+
+ // Some object cache implementations may return non-boolean values.
+ // Verify the store by reading the value back.
+ $stored_value = wp_cache_get( $cache_key, self::CACHE_GROUP );
+ if ( $stored_value === $version ) {
+ return true;
+ }
+
+ // The stored value doesn't match; clean up and report failure.
+ if ( false !== $stored_value ) {
+ wp_cache_delete( $cache_key, self::CACHE_GROUP );
+ }
+ return false;
}
/**
@@ -152,7 +168,10 @@ class VersionStringGenerator {
$this->validate_input( $id );
$cache_key = $this->get_cache_key( $id );
- return wp_cache_delete( $cache_key, self::CACHE_GROUP );
+ $result = wp_cache_delete( $cache_key, self::CACHE_GROUP );
+
+ // Some object cache implementations may return non-boolean values.
+ return ! is_bool( $result ) || $result;
}
/**
diff --git a/plugins/woocommerce/src/Internal/Traits/RestApiCache.php b/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
index 644ef5aedc..2599ff50f6 100644
--- a/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
+++ b/plugins/woocommerce/src/Internal/Traits/RestApiCache.php
@@ -1340,10 +1340,9 @@ trait RestApiCache {
$cache_ttl = $cached_config['cache_ttl'];
$relevant_hooks = $cached_config['relevant_hooks'];
- $found = false;
- $cached = wp_cache_get( $cache_key, self::$cache_group, false, $found );
+ $cached = wp_cache_get( $cache_key, self::$cache_group );
- if ( ! $found || ! is_array( $cached ) || ! array_key_exists( 'data', $cached ) || ! isset( $cached['entity_versions'], $cached['created_at'] ) ) {
+ if ( ! is_array( $cached ) || ! array_key_exists( 'data', $cached ) || ! isset( $cached['entity_versions'], $cached['created_at'] ) ) {
return null;
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Caches/VersionStringGeneratorTest.php b/plugins/woocommerce/tests/php/src/Internal/Caches/VersionStringGeneratorTest.php
index b1dff9ac91..e81adab07d 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Caches/VersionStringGeneratorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Caches/VersionStringGeneratorTest.php
@@ -295,6 +295,100 @@ class VersionStringGeneratorTest extends WC_Unit_Test_Case {
$this->sut->delete_version( '' );
}
+ /**
+ * @testdox delete_version returns true when wp_cache_delete returns a non-boolean value.
+ */
+ public function test_delete_version_returns_true_for_non_bool_cache_delete(): void {
+ global $wp_object_cache;
+ $original_cache = $wp_object_cache;
+
+ try {
+ $mock_cache = $this->createMock( \WP_Object_Cache::class );
+ $mock_cache->method( 'delete' )->willReturn( null );
+ $wp_object_cache = $mock_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $result = $this->sut->delete_version( 'some-id' );
+
+ $this->assertTrue( $result, 'delete_version should return true when wp_cache_delete returns non-boolean' );
+ } finally {
+ $wp_object_cache = $original_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+ }
+
+ /**
+ * @testdox store_version succeeds when wp_cache_set returns non-boolean but the value is correctly stored.
+ */
+ public function test_store_version_succeeds_for_non_bool_cache_set_with_correct_value(): void {
+ global $wp_object_cache;
+ $original_cache = $wp_object_cache;
+
+ try {
+ // Mock that returns null from set() but delegates get() to the real cache,
+ // simulating a non-standard cache that stores correctly but returns null.
+ $mock_cache = $this->createMock( \WP_Object_Cache::class );
+ $mock_cache->method( 'set' )->willReturnCallback(
+ function ( $key, $data, $group, $expire ) use ( $original_cache ) {
+ $original_cache->set( $key, $data, $group, $expire );
+ return null;
+ }
+ );
+ $mock_cache->method( 'get' )->willReturnCallback(
+ function ( $key, $group, $force, &$found ) use ( $original_cache ) {
+ return $original_cache->get( $key, $group, $force, $found );
+ }
+ );
+ $wp_object_cache = $mock_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $version = $this->sut->generate_version( 'test-store-ok' );
+
+ $this->assertNotEmpty( $version, 'generate_version should return a version even when wp_cache_set returns non-boolean' );
+ } finally {
+ $wp_object_cache = $original_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+ }
+
+ /**
+ * @testdox store_version fails and cleans up when wp_cache_set returns non-boolean and the stored value doesn't match.
+ */
+ public function test_store_version_fails_for_non_bool_cache_set_with_wrong_value(): void {
+ global $wp_object_cache;
+ $original_cache = $wp_object_cache;
+
+ try {
+ $mock_cache = $this->createMock( \WP_Object_Cache::class );
+ $mock_cache->method( 'set' )->willReturnCallback(
+ function ( $key, $data, $group, $expire ) use ( $original_cache ) {
+ // Store a wrong value to simulate a corrupted write.
+ $original_cache->set( $key, 'wrong-value', $group, $expire );
+ return null;
+ }
+ );
+ $mock_cache->method( 'get' )->willReturnCallback(
+ function ( $key, $group, $force, &$found ) use ( $original_cache ) {
+ return $original_cache->get( $key, $group, $force, $found );
+ }
+ );
+ $mock_cache->method( 'delete' )->willReturnCallback(
+ function ( $key, $group ) use ( $original_cache ) {
+ return $original_cache->delete( $key, $group );
+ }
+ );
+ $wp_object_cache = $mock_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $version = $this->sut->generate_version( 'test-store-fail' );
+
+ // generate_version still returns a UUID string, but the store failed silently.
+ $this->assertNotEmpty( $version );
+ } finally {
+ $wp_object_cache = $original_cache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ // The corrupted value should have been cleaned up.
+ $cache_key = 'wc_version_string_' . md5( 'test-store-fail' );
+ $cached_value = wp_cache_get( $cache_key, $this->get_cache_group() );
+ $this->assertFalse( $cached_value, 'Mismatched cached value should have been deleted' );
+ }
+
/**
* @testdox Negative TTL from filter is converted to 0 and cache operations still succeed.
*/