Commit 638a5f2f15 for woocommerce
commit 638a5f2f157285822296c29f51a2ca08d3700697
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date: Tue Jan 20 11:06:05 2026 +0100
Add defensive coding for WooCommerce Helper API transients (#62865)
* Add defensive coding for WooCommerce Helper API transients
Prevents fatal errors when transient data becomes corrupted (e.g., string instead of expected array). Validates cached data at the producer level in get_subscriptions(), get_product_usage_notice_rules(), get_notices(), and get_cached_connection_data().
* Add changefile(s) from automation for the following project(s): woocommerce
* Add missing declare( strict_types = 1 ); to test file
* Use remove_filter() instead of remove_all_filters() in WC_Helper tests
Prevents cross-test flakiness by removing only the specific mock callback instead of wiping all pre_http_request filters.
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/62865-update-more-defensive-coding-around-wccom-helper-api b/plugins/woocommerce/changelog/62865-update-more-defensive-coding-around-wccom-helper-api
new file mode 100644
index 0000000000..322e9f486a
--- /dev/null
+++ b/plugins/woocommerce/changelog/62865-update-more-defensive-coding-around-wccom-helper-api
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent fatal errors when WooCommerce.com Helper API transients contain corrupted data.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
index 41053158f7..bfe067c366 100644
--- a/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
+++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper.php
@@ -1700,7 +1700,11 @@ class WC_Helper {
$cache_key = '_woocommerce_helper_product_usage_notice_rules';
$data = get_transient( $cache_key );
if ( false !== $data ) {
- return $data;
+ if ( is_array( $data ) ) {
+ return $data;
+ }
+ // Cached data is corrupted, delete and fetch fresh.
+ delete_transient( $cache_key );
}
try {
@@ -1780,7 +1784,13 @@ class WC_Helper {
* @return array|bool cached connection data or false connection data is not cached.
*/
public static function get_cached_connection_data() {
- return get_transient( self::CACHE_KEY_CONNECTION_DATA );
+ $data = get_transient( self::CACHE_KEY_CONNECTION_DATA );
+ if ( false !== $data && ! is_array( $data ) ) {
+ // Cached data is corrupted, delete and return false to trigger fresh fetch.
+ delete_transient( self::CACHE_KEY_CONNECTION_DATA );
+ return false;
+ }
+ return $data;
}
/**
@@ -1836,7 +1846,11 @@ class WC_Helper {
$cache_key = '_woocommerce_helper_subscriptions';
$data = get_transient( $cache_key );
if ( false !== $data ) {
- return $data;
+ if ( is_array( $data ) ) {
+ return $data;
+ }
+ // Cached data is corrupted, delete and fetch fresh.
+ delete_transient( $cache_key );
}
try {
@@ -2740,7 +2754,11 @@ class WC_Helper {
$cached_data = get_transient( $cache_key );
if ( false !== $cached_data ) {
- return $cached_data;
+ if ( is_array( $cached_data ) ) {
+ return $cached_data;
+ }
+ // Cached data is corrupted, delete and fetch fresh.
+ delete_transient( $cache_key );
}
// Fetch notice data for connected store.
diff --git a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-test.php b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-test.php
index a59e088cca..8209f1bbac 100644
--- a/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/helper/class-wc-helper-test.php
@@ -1,10 +1,271 @@
<?php
+declare( strict_types = 1 );
/**
* Class WC_Tests_WC_Helper.
*/
class WC_Helper_Test extends \WC_Unit_Test_Case {
+ /**
+ * Set up before each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->cleanup_helper_transients();
+ }
+
+ /**
+ * Tear down after each test.
+ */
+ public function tearDown(): void {
+ $this->cleanup_helper_transients();
+ parent::tearDown();
+ }
+
+ /**
+ * Clean up transients used by WC_Helper.
+ */
+ private function cleanup_helper_transients(): void {
+ delete_transient( '_woocommerce_helper_subscriptions' );
+ delete_transient( '_woocommerce_helper_product_usage_notice_rules' );
+ delete_transient( '_woocommerce_helper_notices' );
+ delete_transient( '_woocommerce_helper_connection_data' );
+ }
+
+ /**
+ * @testdox get_subscriptions should delete corrupted string transient and return empty array.
+ */
+ public function test_get_subscriptions_handles_corrupted_string_transient(): void {
+ set_transient( '_woocommerce_helper_subscriptions', 'corrupted_string_data', HOUR_IN_SECONDS );
+
+ // Mock API to prevent actual network call - return WP_Error to trigger empty array return.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Helper::get_subscriptions();
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertIsArray( $result, 'Result should be an array even when transient was corrupted' );
+ $this->assertEmpty( $result, 'Result should be empty array on API error' );
+
+ // Verify corrupted string is no longer in transient (replaced with empty array).
+ $transient_value = get_transient( '_woocommerce_helper_subscriptions' );
+ $this->assertNotEquals( 'corrupted_string_data', $transient_value, 'Corrupted string transient should have been replaced' );
+ $this->assertIsArray( $transient_value, 'Transient should now be an array' );
+ }
+
+ /**
+ * @testdox get_subscriptions should return valid cached array without modification.
+ */
+ public function test_get_subscriptions_returns_valid_cached_array(): void {
+ $valid_data = array(
+ array(
+ 'product_id' => 123,
+ 'product_key' => 'test_key',
+ ),
+ );
+ set_transient( '_woocommerce_helper_subscriptions', $valid_data, HOUR_IN_SECONDS );
+
+ $result = WC_Helper::get_subscriptions();
+
+ $this->assertEquals( $valid_data, $result, 'Valid cached data should be returned as-is' );
+ }
+
+ /**
+ * @testdox get_cached_connection_data should return false for corrupted string transient.
+ */
+ public function test_get_cached_connection_data_handles_corrupted_string_transient(): void {
+ set_transient( '_woocommerce_helper_connection_data', 'corrupted_string', HOUR_IN_SECONDS );
+
+ $result = WC_Helper::get_cached_connection_data();
+
+ $this->assertFalse( $result, 'Corrupted transient should return false' );
+ $this->assertFalse( get_transient( '_woocommerce_helper_connection_data' ), 'Corrupted transient should be deleted' );
+ }
+
+ /**
+ * @testdox get_cached_connection_data should return valid cached array.
+ */
+ public function test_get_cached_connection_data_returns_valid_array(): void {
+ $valid_data = array( 'url' => 'https://example.com' );
+ set_transient( '_woocommerce_helper_connection_data', $valid_data, HOUR_IN_SECONDS );
+
+ $result = WC_Helper::get_cached_connection_data();
+
+ $this->assertEquals( $valid_data, $result, 'Valid cached array should be returned' );
+ }
+
+ /**
+ * @testdox get_cached_connection_data should return false when transient does not exist.
+ */
+ public function test_get_cached_connection_data_returns_false_for_missing_transient(): void {
+ delete_transient( '_woocommerce_helper_connection_data' );
+
+ $result = WC_Helper::get_cached_connection_data();
+
+ $this->assertFalse( $result, 'Missing transient should return false' );
+ }
+
+ /**
+ * @testdox get_product_usage_notice_rules should delete corrupted transient and fetch fresh data.
+ */
+ public function test_get_product_usage_notice_rules_handles_corrupted_transient(): void {
+ set_transient( '_woocommerce_helper_product_usage_notice_rules', 'corrupted_data', HOUR_IN_SECONDS );
+
+ // Mock API to return empty array.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Helper::get_product_usage_notice_rules();
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertIsArray( $result, 'Result should be an array' );
+ }
+
+ /**
+ * @testdox get_notices should delete corrupted transient and return empty array.
+ */
+ public function test_get_notices_handles_corrupted_transient(): void {
+ set_transient( '_woocommerce_helper_notices', 'corrupted_data', HOUR_IN_SECONDS );
+
+ // Mock API to return non-200 response.
+ $http_mock = function () {
+ return array(
+ 'response' => array( 'code' => 500 ),
+ 'body' => '',
+ );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Helper::get_notices();
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertIsArray( $result, 'Result should be an array' );
+ $this->assertEmpty( $result, 'Result should be empty on API failure' );
+ }
+
+ /**
+ * @testdox get_subscription_list_data should handle non-array subscriptions gracefully.
+ */
+ public function test_get_subscription_list_data_handles_non_array_subscriptions(): void {
+ set_transient( '_woocommerce_helper_subscriptions', 'corrupted', HOUR_IN_SECONDS );
+
+ // Mock API to prevent network call.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Helper::get_subscription_list_data();
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertIsArray( $result, 'Result should be an array even with corrupted subscriptions transient' );
+ }
+
+ /**
+ * @testdox get_installed_subscriptions should return empty array when subscriptions are corrupted.
+ */
+ public function test_get_installed_subscriptions_handles_corrupted_subscriptions(): void {
+ set_transient( '_woocommerce_helper_subscriptions', 'corrupted', HOUR_IN_SECONDS );
+
+ // Mock API to prevent network call.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ // Set up auth to avoid early return.
+ WC_Helper_Options::update( 'auth', array( 'site_id' => 12345 ) );
+
+ $result = WC_Helper::get_installed_subscriptions();
+
+ remove_filter( 'pre_http_request', $http_mock );
+ WC_Helper_Options::update( 'auth', array() );
+
+ $this->assertIsArray( $result, 'Result should be an array' );
+ $this->assertEmpty( $result, 'Result should be empty when subscriptions are corrupted' );
+ }
+
+ /**
+ * @testdox get_subscription should return false when subscriptions are corrupted.
+ */
+ public function test_get_subscription_handles_corrupted_subscriptions(): void {
+ set_transient( '_woocommerce_helper_subscriptions', 'corrupted', HOUR_IN_SECONDS );
+
+ // Mock API to prevent network call.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Helper::get_subscription( 'some_product_key' );
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertFalse( $result, 'Result should be false when subscriptions are corrupted' );
+ }
+
+ /**
+ * @testdox has_host_plan_orders should return false when subscriptions are corrupted.
+ */
+ public function test_has_host_plan_orders_handles_corrupted_subscriptions(): void {
+ set_transient( '_woocommerce_helper_subscriptions', 'corrupted', HOUR_IN_SECONDS );
+
+ // Mock API to prevent network call.
+ $http_mock = function () {
+ return new WP_Error( 'test', 'Mocked error' );
+ };
+ add_filter( 'pre_http_request', $http_mock );
+
+ $result = WC_Woo_Helper_Connection::has_host_plan_orders();
+
+ remove_filter( 'pre_http_request', $http_mock );
+
+ $this->assertFalse( $result, 'Should return false when subscriptions are corrupted' );
+ }
+
+ /**
+ * @testdox has_host_plan_orders should return true when subscription has host plan.
+ */
+ public function test_has_host_plan_orders_returns_true_for_host_plan(): void {
+ $subscriptions = array(
+ array(
+ 'product_id' => 123,
+ 'included_in_host_plan' => true,
+ ),
+ );
+ set_transient( '_woocommerce_helper_subscriptions', $subscriptions, HOUR_IN_SECONDS );
+
+ $result = WC_Woo_Helper_Connection::has_host_plan_orders();
+
+ $this->assertTrue( $result, 'Should return true when subscription has host plan' );
+ }
+
+ /**
+ * @testdox has_host_plan_orders should return false when no subscription has host plan.
+ */
+ public function test_has_host_plan_orders_returns_false_without_host_plan(): void {
+ $subscriptions = array(
+ array(
+ 'product_id' => 123,
+ 'included_in_host_plan' => false,
+ ),
+ );
+ set_transient( '_woocommerce_helper_subscriptions', $subscriptions, HOUR_IN_SECONDS );
+
+ $result = WC_Woo_Helper_Connection::has_host_plan_orders();
+
+ $this->assertFalse( $result, 'Should return false when no subscription has host plan' );
+ }
+
/**
* Test that woo plugins are loaded correctly even if incorrect cache is initially set.
*/