PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-external-storage.php

class-external-storage.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-connection/src/class-external-storage.php

321 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * External Storage utilities for Jetpack Connection.
4 *
5 * Provides centralized logic for external storage implementations
6 * across different environments (WoA, VIP, other).
7 *
8 * Usage Example:
9 *
10 * // 1. Create a storage provider class implementing the interface:
11 * class My_Storage_Provider implements Storage_Provider_Interface {
12 * public function is_available() { return true; }
13 * public function should_handle( $option_name ) {
14 * return in_array( $option_name, array( 'blog_token', 'id' ), true );
15 * }
16 * public function get( $option_name ) {
17 * // Return value from your external storage or null
18 * }
19 * public function get_environment_id() { return 'my_env'; }
20 * }
21 *
22 * // 2. Register the provider:
23 * if ( class_exists( 'Automattic\Jetpack\Connection\External_Storage' ) ) {
24 * \Automattic\Jetpack\Connection\External_Storage::register_provider( new My_Storage_Provider() );
25 * }
26 *
27 * // 3. External storage is now automatically used by Jetpack_Options::get_option()
28 *
29 * @package automattic/jetpack-connection
30 */
31
32 namespace Automattic\Jetpack\Connection;
33
34 /**
35 * External Storage utilities class.
36 *
37 * @since 6.18.0
38 */
39 class External_Storage {
40
41 /**
42 * Registered storage provider.
43 *
44 * @since 6.18.0
45 *
46 * @var Storage_Provider_Interface|null
47 */
48 private static $provider = null;
49
50 /**
51 * Whether the init action has already fired.
52 *
53 * @since 8.3.0
54 *
55 * @var bool
56 */
57 private static $init_fired = false;
58
59 /**
60 * Static cache to prevent logging same event multiple times in single request.
61 *
62 * @since 7.0.0
63 *
64 * @var array
65 */
66 private static $logged_events = array();
67
68 /**
69 * Maximum delay threshold for empty state reporting (in seconds).
70 * This also determines the transient expiry for tracking first empty state.
71 * Provider custom thresholds must not exceed this value.
72 *
73 * @since 7.0.0
74 */
75 private const EMPTY_STATE_TRANSIENT_EXPIRY = 15 * MINUTE_IN_SECONDS;
76
77 /**
78 * Register a storage provider for external storage.
79 *
80 * @since 6.18.0
81 *
82 * @param Storage_Provider_Interface $provider Storage provider implementing the interface.
83 * @return bool True if provider was registered successfully, false otherwise.
84 */
85 public static function register_provider( Storage_Provider_Interface $provider ) {
86 self::$provider = $provider;
87
88 /**
89 * Fires after an external storage provider is registered.
90 *
91 * This allows dependent systems (like the connection status cache in Manager)
92 * to invalidate state that may have been computed before the provider was available.
93 *
94 * @since 8.3.0
95 *
96 * @param Storage_Provider_Interface $provider The registered storage provider.
97 */
98 do_action( 'jetpack_external_storage_provider_registered', $provider );
99
100 return true;
101 }
102
103 /**
104 * Get value from external storage provider.
105 *
106 * Returns null if no provider is registered or if the provider can't provide the value (triggers database fallback).
107 *
108 * @since 6.18.0
109 *
110 * @param string $key The key to retrieve.
111 * @return mixed The value from external storage, or null for database fallback.
112 */
113 public static function get_value( $key ) {
114 if ( ! self::$init_fired ) {
115 self::$init_fired = true;
116
117 /**
118 * Fires before the first external storage read.
119 *
120 * Use this hook to register your storage provider via
121 * External_Storage::register_provider(). This fires after the connection
122 * package classes are loaded but before any connection status checks read
123 * from external storage.
124 *
125 * Useful for mu-plugins that load before the plugin providing External_Storage,
126 * since add_action() does not require the action or any classes to exist at
127 * hook-registration time.
128 *
129 * @since 8.3.0
130 */
131 do_action( 'jetpack_external_storage_init' );
132 }
133
134 $provider = self::$provider;
135
136 // Check if we have a registered provider
137 if ( null === $provider ) {
138 return null; // No provider registered, use database
139 }
140
141 $environment = $provider->get_environment_id();
142
143 // Check if provider is available in current environment
144 if ( ! $provider->is_available() ) {
145 self::log_event( 'unavailable', $key, 'External storage not available', $environment );
146 return null;
147 }
148
149 // Check if provider should handle this option
150 if ( ! $provider->should_handle( $key ) ) {
151 return null;
152 }
153
154 // Try to get value from the provider
155 try {
156 $value = $provider->get( $key );
157
158 // Check if we got a valid value
159 if ( null !== $value && false !== $value && '' !== $value && 0 !== $value ) {
160 return $value;
161 }
162
163 // Empty value - log it
164 self::log_event( 'empty', $key, '', $environment );
165
166 } catch ( \Exception $e ) {
167 // Provider threw an exception
168 self::log_event( 'error', $key, $e->getMessage(), $environment );
169 }
170
171 // Provider couldn't provide value, return null for database fallback
172 return null;
173 }
174
175 /**
176 * Log events if WP_DEBUG is enabled and delegate to provider for error reporting.
177 * Includes rate limiting to prevent log spam from noisy events.
178 *
179 * Storage providers can optionally implement handle_error_event() method to receive
180 * notifications about storage errors and empty states for their own error reporting.
181 *
182 * @since 6.18.0
183 *
184 * @param string $event_type The event type (error, empty, unavailable).
185 * @param string $key The key that triggered the event.
186 * @param string $details Additional details about the event.
187 * @param string $environment The environment identifier (atomic, vip, etc.).
188 */
189 public static function log_event( $event_type, $key, $details = '', $environment = 'unknown' ) {
190 // Only process 'error' and 'empty' events for provider error reporting
191 if ( 'error' !== $event_type && 'empty' !== $event_type ) {
192 // For non-reportable events, just do debug logging with rate limiting
193 if ( self::should_log_event( $key, $event_type ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
194 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
195 sprintf(
196 'Jetpack External Storage %s: %s in %s%s',
197 $event_type,
198 $key,
199 $environment,
200 $details ? ' - ' . $details : ''
201 )
202 );
203 }
204 return;
205 }
206
207 // For 'empty' events, check delay mechanism first to avoid false positives
208 // during sync between external storage and the database.
209 // This is checked BEFORE rate limiting so we don't block legitimate reports.
210 if ( 'empty' === $event_type && ! self::should_report_empty_state( $key ) ) {
211 return;
212 }
213
214 // Apply rate limiting only for events that will trigger provider notification
215 if ( ! self::should_log_event( $key, $event_type ) ) {
216 return;
217 }
218
219 // Local debug logging (only when WP_DEBUG is enabled)
220 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
221 error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
222 sprintf(
223 'Jetpack External Storage %s: %s in %s%s',
224 $event_type,
225 $key,
226 $environment,
227 $details ? ' - ' . $details : ''
228 )
229 );
230 }
231
232 // Delegate to provider if it implements error handling
233 if ( null !== self::$provider && method_exists( self::$provider, 'handle_error_event' ) ) {
234 // @phan-suppress-next-line PhanUndeclaredMethod -- Optional method, checked via method_exists()
235 self::$provider->handle_error_event( $event_type, $key, $details, $environment );
236 }
237 }
238
239 /**
240 * Determine if we should report an empty state based on delay mechanism.
241 *
242 * This prevents false positives during storage sync delays. On first encounter
243 * of empty state, sets a transient. On subsequent encounters after the delay
244 * threshold, allows reporting (indicating likely disconnection, not sync delay).
245 *
246 * Providers can customize the delay threshold by implementing get_empty_state_delay_threshold().
247 *
248 * @since 6.18.0
249 *
250 * @param string $key The key that was empty.
251 * @return bool True if we should report this empty state, false otherwise.
252 */
253 private static function should_report_empty_state( $key ) {
254 $delay_key = 'jetpack_external_storage_empty_delay_' . $key;
255 $first_empty_time = get_transient( $delay_key );
256
257 if ( false === $first_empty_time ) {
258 // First time encountering empty state - set delay transient and don't report yet
259 set_transient( $delay_key, time(), self::EMPTY_STATE_TRANSIENT_EXPIRY );
260 return false;
261 }
262
263 // Default delay threshold (5 minutes)
264 $delay_threshold = 5 * MINUTE_IN_SECONDS;
265
266 // Allow provider to customize delay threshold
267 // A threshold of 0 is valid for providers where external storage is written first
268 if ( null !== self::$provider && method_exists( self::$provider, 'get_empty_state_delay_threshold' ) ) {
269 // @phan-suppress-next-line PhanUndeclaredMethod -- Optional method, checked via method_exists()
270 $custom_threshold = self::$provider->get_empty_state_delay_threshold();
271 if ( is_int( $custom_threshold ) && $custom_threshold >= 0 && $custom_threshold <= self::EMPTY_STATE_TRANSIENT_EXPIRY ) {
272 $delay_threshold = $custom_threshold;
273 }
274 }
275
276 if ( ( time() - $first_empty_time ) >= $delay_threshold ) {
277 // Delay threshold passed - likely disconnection, report it
278 delete_transient( $delay_key );
279 return true;
280 }
281
282 return false;
283 }
284
285 /**
286 * Determine if an event should be logged based on rate limiting rules.
287 *
288 * This prevents log spam from noisy events by applying a simple one-hour
289 * rate limit per key and event type combination. Also uses a static cache
290 * to prevent duplicate logs within the same request.
291 *
292 * @since 6.18.0
293 *
294 * @param string $key The key that triggered the event.
295 * @param string $event_type The event type (error, empty, unavailable).
296 * @return bool True if the event should be logged, false if rate limited.
297 */
298 private static function should_log_event( $key, $event_type = '' ) {
299 // Combine event type and key for unique tracking
300 $event_cache_key = $event_type . '_' . $key;
301
302 // Check static cache first (prevents multiple logs in same request)
303 if ( isset( self::$logged_events[ $event_cache_key ] ) ) {
304 return false;
305 }
306
307 $rate_limit_key = 'jetpack_ext_storage_rate_limit_' . $event_cache_key;
308
309 // Check if we're still within the rate limit period
310 if ( get_transient( $rate_limit_key ) ) {
311 return false;
312 }
313
314 // Mark as logged in both caches
315 self::$logged_events[ $event_cache_key ] = true;
316 set_transient( $rate_limit_key, true, HOUR_IN_SECONDS );
317
318 return true;
319 }
320 }
321