diff --git a/changelog.txt b/changelog.txt index 2ef67a0ba0..bd4e2bbb9b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,7 @@ * Dev - Renames previous Order Helper class methods to use the `_id` suffix * Dev - Expands the Stripe Order Helper class to handle customer ID, card ID, UPE payment type, and UPE redirect status metas * Fix - Remove persistent reconnection notices +* Add - Allow cache prefetch window to be adjusted via the wc_stripe_database_cache_prefetch_window filter = 10.0.0 - 2025-10-14 = * Update - Removes frontend code related to Payment Request Buttons in the checkout page diff --git a/includes/class-wc-stripe-database-cache-prefetch.php b/includes/class-wc-stripe-database-cache-prefetch.php index 46c62a81d2..a9dc7ac4f1 100644 --- a/includes/class-wc-stripe-database-cache-prefetch.php +++ b/includes/class-wc-stripe-database-cache-prefetch.php @@ -60,7 +60,7 @@ public static function get_instance(): WC_Stripe_Database_Cache_Prefetch { * @return bool True if the cache key can be prefetched, false otherwise. */ public function should_prefetch_cache_key( string $key ): bool { - return isset( self::PREFETCH_CONFIG[ $key ] ) && self::PREFETCH_CONFIG[ $key ] > 0; + return $this->get_prefetch_window( $key ) > 0; } /** @@ -70,12 +70,11 @@ public function should_prefetch_cache_key( string $key ): bool { * @param int $expiry_time The expiry time of the cache entry. */ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { - if ( ! $this->should_prefetch_cache_key( $key ) ) { + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { return; } - $prefetch_window = self::PREFETCH_CONFIG[ $key ]; - // If now plus the prefetch window is before the expiry time, do not trigger a prefetch. if ( ( time() + $prefetch_window ) < $expiry_time ) { return; @@ -107,6 +106,39 @@ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { } } + /** + * Get the prefetch window for a given cache key. + * + * @param string $key The unprefixed cache key to get the prefetch window for. + * @return int The prefetch window for the cache key. 0 indicates that prefetching is disabled for the key. + */ + private function get_prefetch_window( string $cache_key ): int { + if ( ! isset( self::PREFETCH_CONFIG[ $cache_key ] ) ) { + return 0; + } + + $initial_prefetch_window = self::PREFETCH_CONFIG[ $cache_key ]; + + /** + * Filters the cache prefetch window for a given cache key. Return 0 or less to disable prefetching for the key. + * + * @param int $prefetch_window The prefetch window for the cache key. + * @param string $cache_key The unprefixed cache key. + */ + $prefetch_window = apply_filters( 'wc_stripe_database_cache_prefetch_window', $initial_prefetch_window, $cache_key ); + + // If the filter returns a non-integer, use the initial prefetch window. + if ( ! is_int( $prefetch_window ) ) { + return $initial_prefetch_window; + } + + if ( $prefetch_window <= 0 ) { + return 0; + } + + return $prefetch_window; + } + /** * Check if a prefetch is already queued up. * @@ -114,7 +146,8 @@ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { * @return bool True if a prefetch is queued up, false otherwise. */ private function is_prefetch_queued( string $key ): bool { - if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { return false; } @@ -126,8 +159,7 @@ private function is_prefetch_queued( string $key ): bool { return false; } - $now = time(); - $prefetch_window = self::PREFETCH_CONFIG[ $key ]; + $now = time(); if ( $prefetch_option >= ( $now - $prefetch_window ) ) { // If the prefetch entry expires in the future, or falls within the prefetch window for the key, we should consider the item live and queued. @@ -161,13 +193,14 @@ public function handle_prefetch_action( $key ): void { 'Invalid cache prefetch key', [ 'cache_key' => $key, - 'reason' => 'invalid_key', + 'reason' => 'invalid_cache_key', ] ); return; } - if ( ! $this->should_prefetch_cache_key( $key ) ) { + // We don't use should_prefetch_cache_key(), as that calls get_prefetch_window(), which we're checking below. + if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { WC_Stripe_Logger::warning( 'Invalid cache prefetch key', [ @@ -178,6 +211,18 @@ public function handle_prefetch_action( $key ): void { return; } + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { + WC_Stripe_Logger::warning( + 'Cache prefetch key was disabled', + [ + 'cache_key' => $key, + 'reason' => 'cache_key_disabled', + ] + ); + return; + } + $this->prefetch_cache_key( $key ); // Regardless of whether the prefetch was successful or not, we should remove the prefetch tracking option. diff --git a/readme.txt b/readme.txt index e5d27fe25a..ec5f9e8af2 100644 --- a/readme.txt +++ b/readme.txt @@ -114,5 +114,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Dev - Renames previous Order Helper class methods to use the `_id` suffix * Dev - Expands the Stripe Order Helper class to handle customer ID, card ID, UPE payment type, and UPE redirect status metas * Fix - Remove persistent reconnection notices +* Add - Allow cache prefetch window to be adjusted via the wc_stripe_database_cache_prefetch_window filter [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). diff --git a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php index 9f0770bb6b..d23207d934 100644 --- a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php +++ b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php @@ -18,25 +18,45 @@ class WC_Stripe_Database_Cache_Prefetch_Test extends \WP_UnitTestCase { */ public function provide_handle_prefetch_action_test_cases(): array { return [ - 'pmc_key_exists_and_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true ], - 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', false ], + 'pmc_key_exists_and_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, null, true ], + 'pmc_key_exists_and_should_prefetch_with_20_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 20, true ], + 'pmc_key_exists_and_should_not_prefetch_with_0_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 0, false ], + 'pmc_key_exists_and_should_not_prefetch_with_negative_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, -3, false ], + 'pmc_key_exists_and_should_prefetch_with_invalid_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 'invalid', true ], + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', null, false ], + 'invalid_key_should_not_prefetch_with_filter' => [ 'invalid_test_key', 10, false ], + 'invalid_key_should_not_prefetch_with_0_filter' => [ 'invalid_test_key', 0, false ], + 'invalid_key_should_not_prefetch_with_negative_filter' => [ 'invalid_test_key', -3, false ], + 'invalid_key_should_not_prefetch_with_invalid_filter' => [ 'invalid_test_key', 'invalid', false ], ]; } /** * Test {@see \WC_Stripe_Database_Cache_Prefetch::handle_prefetch_action()}. * - * @param string $key The key to prefetch. - * @param bool $should_prefetch Whether we expect the key to be prefetched. + * @param string $key The key to prefetch. + * @param mixed $prefetch_window_filter_value The value to filter the prefetch window with. Null is no filter value set; other values will be returned as-is. + * @param bool $should_prefetch Whether we expect the key to be prefetched. * * @dataProvider provide_handle_prefetch_action_test_cases */ - public function test_handle_prefetch_action( string $key, bool $should_prefetch ): void { + public function test_handle_prefetch_action( string $key, $prefetch_window_filter_value, bool $should_prefetch ): void { $mock_instance = $this->getMockBuilder( 'WC_Stripe_Database_Cache_Prefetch' ) ->disableOriginalConstructor() ->onlyMethods( [ 'prefetch_cache_key' ] ) ->getMock(); + $filter_callback = null; + if ( null !== $prefetch_window_filter_value ) { + $filter_callback = function ( $prefetch_window, $cache_key ) use ( $key, $prefetch_window_filter_value ) { + if ( $cache_key === $key ) { + return $prefetch_window_filter_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + $expected_prefetch_count = $should_prefetch ? $this->once() : $this->never(); $mock_instance->expects( $expected_prefetch_count ) @@ -45,6 +65,10 @@ public function test_handle_prefetch_action( string $key, bool $should_prefetch ->willReturn( true ); $mock_instance->handle_prefetch_action( $key ); + + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } } /** @@ -54,13 +78,18 @@ public function test_handle_prefetch_action( string $key, bool $should_prefetch */ public function provide_maybe_queue_prefetch_test_cases(): array { return [ - 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', 5, false ], - 'pmc_key_expires_in_60_seconds_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 60, false ], - 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], - 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], - 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], - 'pmc_key_expires_in_5_seconds_with_option_set_-11s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -11 ], - 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', 5, false ], + 'pmc_key_expires_in_60_seconds_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 60, false ], + 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], + 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-11s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -11 ], + 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], + 'pmc_key_expires_in_5_seconds_with_20_filter_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, null, 20 ], + 'pmc_key_expires_in_5_seconds_with_20_filter_option_-11_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -11, 20 ], + 'pmc_key_expires_in_5_seconds_with_0_filter_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, null, 0 ], + 'pmc_key_expires_in_5_seconds_with_negative_filter_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, null, -3 ], + 'pmc_key_expires_in_5_seconds_with_invalid_filter_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, null, 'invalid' ], ]; } @@ -74,7 +103,7 @@ public function provide_maybe_queue_prefetch_test_cases(): array { * * @dataProvider provide_maybe_queue_prefetch_test_cases */ - public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustment, bool $should_enqueue_action, $option_adjusted_time = null ): void { + public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustment, bool $should_enqueue_action, $option_adjusted_time = null, $prefetch_window_filter_value = null ): void { $instance = \WC_Stripe_Database_Cache_Prefetch::get_instance(); $mock_class = $this->getMockBuilder( \stdClass::class ) @@ -88,6 +117,17 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm add_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10, 4 ); + $filter_callback = null; + if ( null !== $prefetch_window_filter_value ) { + $filter_callback = function ( $prefetch_window, $cache_key ) use ( $key, $prefetch_window_filter_value ) { + if ( $cache_key === $key ) { + return $prefetch_window_filter_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + $test_args = [ $key, $expiry_time_adjustment, $should_enqueue_action, $option_adjusted_time ]; $option_name = 'wcstripe_prefetch_' . $key; @@ -112,6 +152,9 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm $end_time = time(); remove_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10 ); + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } $option_value = get_option( $option_name, false ); @@ -127,4 +170,51 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm $this->assertEquals( $initial_option_value, $option_value ); } } + + /** + * Provide test cases for {@see test_should_prefetch_cache_key()}. + * + * @return array + */ + public function provide_test_should_prefetch_cache_key_test_cases(): array { + return [ + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', false, null ], + 'pmc_key_should_prefetch_with_no_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, null ], + 'pmc_key_should_not_prefetch_with_0_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, false, 0 ], + 'pmc_key_should_prefetch_with_20_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, 20 ], + 'pmc_key_should_not_prefetch_with_negative_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, false, -5 ], + 'pmc_key_should_prefetch_with_invalid_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, 'invalid' ], + ]; + } + + /** + * Test {@see \WC_Stripe_Database_Cache_Prefetch::should_prefetch_cache_key()}. + * + * @param string $cache_key The key to prefetch. + * @param bool $expected_result The expected result. + * @param mixed $filter_return_value The value to return from the prefetch window filter. Null is no filter value returned; other values will be returned as-is. + * + * @dataProvider provide_test_should_prefetch_cache_key_test_cases + */ + public function test_should_prefetch_cache_key( string $cache_key, bool $expected_result, $filter_return_value = null ): void { + $instance = \WC_Stripe_Database_Cache_Prefetch::get_instance(); + + $filter_callback = null; + if ( null !== $filter_return_value ) { + $filter_callback = function ( $prefetch_window, $key ) use ( $cache_key, $filter_return_value ) { + if ( $cache_key === $key ) { + return $filter_return_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + + $result = $instance->should_prefetch_cache_key( $cache_key ); + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } + + $this->assertEquals( $expected_result, $result ); + } }