diff --git a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php index 6e7d9b1487b..80d518e03f2 100644 --- a/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php +++ b/plugins/woocommerce-beta-tester/api/remote-logging/remote-logging.php @@ -74,6 +74,7 @@ function toggle_remote_logging( $request ) { update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' ); update_option( 'woocommerce_allow_tracking', 'yes' ); update_option( 'woocommerce_remote_variant_assignment', 1 ); + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, WC()->version ); } else { update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ); } diff --git a/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check b/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check new file mode 100644 index 00000000000..6807f1d962f --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-move-woo-version-check @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Update remote logger tool to toggle remote logging feature properly diff --git a/plugins/woocommerce/changelog/update-move-woo-version-check b/plugins/woocommerce/changelog/update-move-woo-version-check new file mode 100644 index 00000000000..af5e8c0070e --- /dev/null +++ b/plugins/woocommerce/changelog/update-move-woo-version-check @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Enhance WooCommerce version checking for remote logging reliability diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php index 01e71d40e62..3bcb44f4c29 100644 --- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php +++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php @@ -20,11 +20,10 @@ use WC_Log_Levels; * @package WooCommerce\Classes */ class RemoteLogger extends \WC_Log_Handler { - const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; - const RATE_LIMIT_ID = 'woocommerce_remote_logging'; - const RATE_LIMIT_DELAY = 60; // 1 minute. - const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version'; - const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry'; + const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; + const RATE_LIMIT_ID = 'woocommerce_remote_logging'; + const RATE_LIMIT_DELAY = 60; // 1 minute. + const WC_NEW_VERSION_TRANSIENT = 'woocommerce_new_version'; /** * Handle a log entry. @@ -150,7 +149,7 @@ class RemoteLogger extends \WC_Log_Handler { return false; } - if ( ! $this->is_latest_woocommerce_version() ) { + if ( ! $this->should_current_version_be_logged() ) { return false; } @@ -221,7 +220,7 @@ class RemoteLogger extends \WC_Log_Handler { self::LOG_ENDPOINT, array( 'body' => wp_json_encode( $body ), - 'timeout' => 2, + 'timeout' => 3, 'headers' => array( 'Content-Type' => 'application/json', ), @@ -256,14 +255,22 @@ class RemoteLogger extends \WC_Log_Handler { * * @return bool */ - private function is_latest_woocommerce_version() { - $latest_wc_version = $this->fetch_latest_woocommerce_version(); + private function should_current_version_be_logged() { + $new_version = get_site_transient( self::WC_NEW_VERSION_TRANSIENT ); - if ( is_null( $latest_wc_version ) ) { - return false; + if ( false === $new_version ) { + $new_version = $this->fetch_new_woocommerce_version(); + // Cache the new version for a week since we want to keep logging in with the same version for a while even if the new version is available. + set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS ); } - return version_compare( WC()->version, $latest_wc_version, '>=' ); + if ( ! is_string( $new_version ) || '' === $new_version ) { + // If the new version is not available, we consider the current version to be the latest. + return true; + } + + // If the current version is the latest, we don't want to log errors. + return version_compare( WC()->version, $new_version, '>=' ); } /** @@ -316,45 +323,34 @@ class RemoteLogger extends \WC_Log_Handler { } /** - * Fetch the latest WooCommerce version using the WordPress API and cache it. + * Fetch the new version of WooCommerce from the WordPress API. * - * @return string|null + * @return string|null New version if an update is available, null otherwise. */ - private function fetch_latest_woocommerce_version() { - $cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT ); - if ( $cached_version ) { - return $cached_version; + private function fetch_new_woocommerce_version() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( ! function_exists( 'get_plugin_updates' ) ) { + require_once ABSPATH . 'wp-admin/includes/update.php'; } - $retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY ); - if ( false === $retry_count || ! is_numeric( $retry_count ) ) { - $retry_count = 0; - } + $plugin_updates = get_plugin_updates(); - if ( $retry_count >= 3 ) { + // Check if WooCommerce plugin update information is available. + if ( ! is_array( $plugin_updates ) || ! isset( $plugin_updates[ WC_PLUGIN_BASENAME ] ) ) { return null; } - if ( ! function_exists( 'plugins_api' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; - } - // Fetch the latest version from the WordPress API. - $plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) ); + $wc_plugin_update = $plugin_updates[ WC_PLUGIN_BASENAME ]; - if ( is_wp_error( $plugin_info ) ) { - ++$retry_count; - set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS ); + // Ensure the update object exists and has the required information. + if ( ! $wc_plugin_update || ! isset( $wc_plugin_update->update->new_version ) ) { return null; } - if ( ! empty( $plugin_info->version ) ) { - $latest_version = $plugin_info->version; - set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS ); - delete_transient( self::FETCH_LATEST_VERSION_RETRY ); - return $latest_version; - } - - return null; + $new_version = $wc_plugin_update->update->new_version; + return is_string( $new_version ) ? $new_version : null; } /** diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php index 7133f38f5ef..63327438a03 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php @@ -35,8 +35,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { public function tearDown(): void { $this->cleanup_filters(); delete_option( 'woocommerce_feature_remote_logging_enabled' ); - delete_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT ); - delete_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ); + delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" ); WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP ); @@ -56,6 +55,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { 'plugins_api', 'pre_http_request', 'woocommerce_remote_logger_formatted_log_data', + 'pre_site_transient_update_plugins', ); foreach ( $filters as $filter ) { remove_all_filters( $filter ); @@ -90,18 +90,23 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { */ public function remote_logging_disallowed_provider() { return array( - 'feature flag disabled' => array( + 'feature flag disabled' => array( 'condition' => 'feature flag disabled', 'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ), ), - 'tracking opted out' => array( + 'tracking opted out' => array( 'condition' => 'tracking opted out', 'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ), ), - 'outdated version' => array( - 'condition' => 'outdated version', - 'setup' => function () { + 'high variant assignment' => array( + 'condition' => 'high variant assignment', + 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), + ), + 'outdated version' => array( + 'condition' => 'outdated version', + 'setup' => function () { $version = WC()->version; + // Next major version. (e.g. 9.0.1 -> 10.0.0). $next_version = implode( '.', array_map( @@ -112,28 +117,79 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { array_keys( explode( '.', $version ) ) ) ); - set_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT, $next_version ); + + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS ); }, - 'high variant assignment' => array( - 'condition' => 'high variant assignment', - 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ), - ), ), ); } - /** - * @testdox Fetch latest WooCommerce version retries on API failure - */ - public function test_fetch_latest_woocommerce_version_retry() { - $this->setup_remote_logging_conditions( true ); - add_filter( 'plugins_api', fn() => new \WP_Error(), 10, 3 ); - for ( $i = 1; $i <= 4; $i++ ) { - $this->sut->is_remote_logging_allowed(); - $retry_count = get_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ); - $this->assertEquals( min( $i, 3 ), $retry_count ); + /** + * @testdox should_current_version_be_logged method behaves correctly + * @dataProvider should_current_version_be_logged_provider + * + * @param string $current_version The current WooCommerce version. + * @param string $new_version The new WooCommerce version. + * @param string $transient_value The value of the transient. + * @param bool $expected The expected result. + */ + public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) { + $wc_version = WC()->version; + WC()->version = $current_version; + + // Set up the transient. + if ( null !== $transient_value ) { + set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS ); + } else { + delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + + $this->setup_mock_plugin_updates( $new_version ); } + + $result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() ); + $this->assertEquals( $expected, $result ); + + // Clean up. + delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT ); + + WC()->version = $wc_version; + } + + /** + * Data provider for test_should_current_version_be_logged. + */ + public function should_current_version_be_logged_provider() { + return array( + 'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ), + 'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ), + 'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ), + 'new version is null (transient set)' => array( '9.2.0', null, null, true ), + 'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ), + 'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ), + 'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ), + 'transient not set, new version is null' => array( '9.2.0', null, null, true ), + ); + } + + /** + * @testdox fetch_new_woocommerce_version method returns correct version + */ + public function test_fetch_new_woocommerce_version() { + $this->setup_mock_plugin_updates( '9.3.0' ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' ); + } + + /** + * @testdox fetch_new_woocommerce_version method returns null when no update is available + */ + public function test_fetch_new_woocommerce_version_no_update() { + add_filter( 'pre_site_transient_update_plugins', fn() => array() ); + + $result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() ); + $this->assertNull( $result, 'The result should be null when no update is available.' ); } /** @@ -421,17 +477,26 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case { update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' ); add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' ); add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 ); - add_filter( - 'plugins_api', - function ( $result, $action, $args ) use ( $enabled ) { - if ( 'plugin_information' === $action && 'woocommerce' === $args->slug ) { - return (object) array( 'version' => $enabled ? WC()->version : '9.0.0' ); - } - return $result; - }, - 10, - 3 + $this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' ); + } + + + /** + * Set up mock plugin updates. + * + * @param string $new_version The new version of WooCommerce to simulate. + */ + private function setup_mock_plugin_updates( $new_version ) { + $update_plugins = (object) array( + 'response' => array( + WC_PLUGIN_BASENAME => (object) array( + 'new_version' => $new_version, + 'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip', + 'slug' => 'woocommerce', + ), + ), ); + add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins ); } /**