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.
 	 */