Skip to content

Commit 69515f1

Browse files
committed
Merge remote-tracking branch 'origin/release/9.5.2' into trunk
2 parents 1dff874 + 1758581 commit 69515f1

16 files changed

+683
-130
lines changed

changelog.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
*** Changelog ***
22

3+
= 9.5.2 - 2025-05-22 =
4+
* Add - Implement custom database cache for persistent caching with in-memory optimization.
5+
* Update - Remove feature that flags 401s and proactively blocks subsequent API calls until the store has reauthenticated.
6+
* Fix - Disable payment settings sync when we receive unsupported payment method configurations.
7+
* Fix - Ensure that we use current Stripe API keys after settings updates
8+
* Fix - Fix initial enabled payment methods migration to the Stripe Payment Methods Configuration API
9+
310
= 9.5.1 - 2025-05-17 =
411
* Fix - Add a fetch cooldown to the payment method configuration retrieval endpoint to prevent excessive requests.
512
* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).

includes/admin/class-wc-rest-stripe-account-keys-controller.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ public function set_account_keys( WP_REST_Request $request ) {
272272
if ( $is_deleting_account ) {
273273
$settings['enabled'] = 'no';
274274
$settings['connection_type'] = '';
275+
$settings['pmc_enabled'] = '';
275276
$settings['test_connection_type'] = '';
276277
$settings['refresh_token'] = '';
277278
$settings['test_refresh_token'] = '';

includes/class-wc-stripe-api.php

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,6 @@ class WC_Stripe_API {
1616
const ENDPOINT = 'https://api.stripe.com/v1/';
1717
const STRIPE_API_VERSION = '2024-06-20';
1818

19-
/**
20-
* The test mode invalid API keys option key.
21-
*
22-
* @var string
23-
*/
24-
const TEST_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_test_invalid_api_keys_detected';
25-
26-
/**
27-
* The live mode invalid API keys option key.
28-
*
29-
* @var string
30-
*/
31-
const LIVE_MODE_INVALID_API_KEYS_OPTION_KEY = 'wc_stripe_live_invalid_api_keys_detected';
32-
3319
/**
3420
* Secret API Key.
3521
*
@@ -245,13 +231,6 @@ public static function request( $request, $api = 'charges', $method = 'POST', $w
245231
* @param string $api
246232
*/
247233
public static function retrieve( $api ) {
248-
// If we have an option flag indicating that the secret key is not valid, we don't attempt the API call and we return an error.
249-
$invalid_api_keys_option_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_INVALID_API_KEYS_OPTION_KEY : self::LIVE_MODE_INVALID_API_KEYS_OPTION_KEY;
250-
$invalid_api_keys_detected = get_option( $invalid_api_keys_option_key );
251-
if ( $invalid_api_keys_detected ) {
252-
return null; // The UI expects this empty response in case of invalid API keys.
253-
}
254-
255234
WC_Stripe_Logger::log( "{$api}" );
256235

257236
$response = wp_safe_remote_get(
@@ -265,12 +244,8 @@ public static function retrieve( $api ) {
265244

266245
// If we get a 401 error, we know the secret key is not valid.
267246
if ( is_array( $response ) && isset( $response['response'] ) && is_array( $response['response'] ) && isset( $response['response']['code'] ) && 401 === $response['response']['code'] ) {
268-
// We save a flag in the options to avoid making calls until the secret key gets updated.
269-
update_option( $invalid_api_keys_option_key, true );
270-
update_option( $invalid_api_keys_option_key . '_at', time() );
271-
272-
// We delete the transient for the account data to trigger the not-connected UI in the admin dashboard.
273-
delete_transient( WC_Stripe_Mode::is_test() ? WC_Stripe_Account::TEST_ACCOUNT_OPTION : WC_Stripe_Account::LIVE_ACCOUNT_OPTION );
247+
// Stripe redacts API keys in the response.
248+
WC_Stripe_Logger::log( "Error: GET {$api} returned a 401" );
274249

275250
return null; // The UI expects this empty response in case of invalid API keys.
276251
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
defined( 'ABSPATH' ) || exit; // block direct access.
4+
5+
/**
6+
* Class WC_Stripe_Database_Cache
7+
*/
8+
9+
/**
10+
* A class for caching data as an option in the database.
11+
*
12+
* Based on the WooCommerce Payments Database_Cache class implementation.
13+
*
14+
* @see https://github.com/Automattic/woocommerce-payments/blob/4b084af108cac9c6bd2467e52e5cdc3bc974a951/includes/class-database-cache.php
15+
*/
16+
class WC_Stripe_Database_Cache {
17+
18+
/**
19+
* In-memory cache for the duration of a single request.
20+
*
21+
* This is used to avoid multiple database reads for the same data and as a backstop in case the database write fails.
22+
*
23+
* @var array
24+
*/
25+
private static $in_memory_cache = [];
26+
27+
/**
28+
* Class constructor.
29+
*/
30+
private function __construct() {
31+
}
32+
33+
/**
34+
* Stores a value in the cache.
35+
*
36+
* @param string $key The key to store the value under.
37+
* @param mixed $data The value to store.
38+
* @param int $ttl The TTL of the cache. Dafault 1 hour.
39+
*
40+
* @return void
41+
*/
42+
public static function set( $key, $data, $ttl = HOUR_IN_SECONDS ) {
43+
self::write_to_cache( $key, $data, $ttl );
44+
}
45+
46+
/**
47+
* Gets a value from the cache.
48+
*
49+
* @param string $key The key to look for.
50+
*
51+
* @return mixed|null The cache contents. NULL if the cache value is expired or missing.
52+
*/
53+
public static function get( $key ) {
54+
$cache_contents = self::get_from_cache( $key );
55+
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) ) {
56+
if ( self::is_expired( $key, $cache_contents ) ) {
57+
return null;
58+
}
59+
60+
return $cache_contents['data'];
61+
}
62+
63+
return null;
64+
}
65+
66+
/**
67+
* Deletes a value from the cache.
68+
*
69+
* @param string $key The key to delete.
70+
*
71+
* @return void
72+
*/
73+
public static function delete( $key ) {
74+
// Remove from the in-memory cache.
75+
unset( self::$in_memory_cache[ $key ] );
76+
77+
// Remove from the DB cache.
78+
if ( delete_option( $key ) ) {
79+
// Clear the WP object cache to ensure the new data is fetched by other processes.
80+
wp_cache_delete( $key, 'options' );
81+
}
82+
}
83+
84+
/**
85+
* Wraps the data in the cache metadata and stores it.
86+
*
87+
* @param string $key The key to store the data under.
88+
* @param mixed $data The data to store.
89+
* @param int $ttl The TTL of the cache.
90+
*
91+
* @return void
92+
*/
93+
private static function write_to_cache( $key, $data, $ttl ) {
94+
// Add the data and expiry time to the array we're caching.
95+
$cache_contents = [
96+
'data' => $data,
97+
'ttl' => $ttl,
98+
'updated' => time(),
99+
];
100+
101+
// Write the in-memory cache.
102+
self::$in_memory_cache[ $key ] = $cache_contents;
103+
104+
// Create or update the DB option cache.
105+
// Note: Since we are adding the current time to the option value, WP will ALWAYS write the option because
106+
// the cache contents value is different from the current one, even if the data is the same.
107+
// A `false` result ONLY means that the DB write failed.
108+
// Yes, there is the possibility that we attempt to write the same data multiple times within the SAME second,
109+
// and we will mistakenly think that the DB write failed. We are OK with this false positive,
110+
// since the actual data is the same.
111+
//
112+
// Note 2: Autoloading too many options can lead to performance problems, and we are implementing this as a
113+
// general cache for the plugin, so we set the autoload to false.
114+
$result = update_option( $key, $cache_contents, false );
115+
if ( false !== $result ) {
116+
// If the DB cache write succeeded, clear the WP object cache to ensure the new data is fetched by other processes.
117+
wp_cache_delete( $key, 'options' );
118+
}
119+
}
120+
121+
/**
122+
* Get the cache contents for a certain key.
123+
*
124+
* @param string $key The cache key.
125+
*
126+
* @return array|false The cache contents (array with `data`, `ttl`, and `updated` entries).
127+
* False if there is no cached data.
128+
*/
129+
private static function get_from_cache( $key ) {
130+
// Check the in-memory cache first.
131+
if ( isset( self::$in_memory_cache[ $key ] ) ) {
132+
return self::$in_memory_cache[ $key ];
133+
}
134+
135+
// Read from the DB cache.
136+
$data = get_option( $key );
137+
138+
// Store the data in the in-memory cache, including the case when there is no data cached (`false`).
139+
self::$in_memory_cache[ $key ] = $data;
140+
141+
return $data;
142+
}
143+
144+
/**
145+
* Checks if the cache value is expired.
146+
*
147+
* @param string $key The cache key.
148+
* @param array $cache_contents The cache contents.
149+
*
150+
* @return boolean True if the contents are expired. False otherwise.
151+
*/
152+
private static function is_expired( $key, $cache_contents ) {
153+
if ( ! is_array( $cache_contents ) || ! isset( $cache_contents['updated'] ) || ! isset( $cache_contents['ttl'] ) ) {
154+
// Treat bad/invalid cache contents as expired
155+
return true;
156+
}
157+
158+
// Double-check that we have integers for `updated` and `ttl`.
159+
if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) {
160+
return true;
161+
}
162+
163+
$expires = $cache_contents['updated'] + $cache_contents['ttl'];
164+
$now = time();
165+
166+
return apply_filters( 'wcstripe_database_cache_is_expired', $expires < $now, $key, $cache_contents );
167+
}
168+
169+
/**
170+
* Get all cached keys in memory.
171+
*
172+
* @return array The cached keys.
173+
*/
174+
public static function get_cached_keys() {
175+
return array_keys( self::$in_memory_cache );
176+
}
177+
}

0 commit comments

Comments
 (0)