PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 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 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-connection/src/class-external-storage.php +320 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,320 @@
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 +}