PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/class-dedicated-sender.php +56 -30 13.3.3 → 16.3 View file →
@@ -42,9 +42,9 @@
42 42 * What's the timeout for the request lock in seconds.
43 43 *
44 44 * 5 seconds as default value seems sane, but we might want to adjust that in the future.
45 45 */
46 - const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
46 + const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 60;
47 47
48 48 /**
49 49 * The query parameter name to use when passing the current lock id.
50 50 */
@@ -84,9 +84,9 @@
84 84 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
85 85 return false;
86 86 }
87 87
88 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
88 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Only substring-matched against a fixed literal; never output or stored.
89 89 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_SERVER['REQUEST_URI'] ) );
90 90 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
91 91 return true;
92 92 }
@@ -96,13 +96,13 @@
96 96 * Sometimes, like when permalinks are disabled, the REST path is sent via the `rest_route` GET parameter.
97 97 * We want to check it too, to make sure we managed to cover more cases and be more certain we actually
98 98 * catch calls to the endpoint.
99 99 */
100 - if ( ! isset( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
100 + if ( ! isset( $_GET['rest_route'] ) || ! is_string( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
101 101 return false;
102 102 }
103 103
104 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
104 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended -- Only substring-matched against a fixed literal; never output or stored.
105 105 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_GET['rest_route'] ) );
106 106 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
107 107 return true;
108 108 }
@@ -132,8 +132,12 @@
132 132 if ( $queue->size() === 0 ) {
133 133 return new WP_Error( 'empty_queue_' . $queue->id );
134 134 }
135 135
136 + if ( get_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME ) ) {
137 + return new WP_Error( 'sender_temporarily_disabled_while_pulling' );
138 + }
139 +
136 140 // Return early if we've gotten a retry-after header response that is not expired.
137 141 $retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
138 142 if ( $retry_time && $retry_time >= microtime( true ) ) {
139 143 return new WP_Error( 'retry_after_' . $queue->id );
@@ -200,43 +204,52 @@
200 204 *
201 205 * To avoid spawning multiple requests at the same time, we need to have a quick lock that will
202 206 * allow only a single request to continue if we try to spawn multiple at the same time.
203 207 *
204 - * @return false|mixed|string
208 + * @return string|false
205 209 */
206 210 public static function try_lock_spawn_request() {
207 - $current_microtime = (string) microtime( true );
211 + $option_name = self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
212 + $expires_name = $option_name . '_expires';
213 + $ttl = self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT;
214 + $lock_id = wp_generate_uuid4();
215 + $now = microtime( true );
208 216
217 + // Fast path: external object cache is atomic.
209 218 if ( wp_using_ext_object_cache() ) {
210 - if ( true !== wp_cache_add( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, 'jetpack', self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
211 - // Cache lock has been claimed already.
212 - return false;
219 + if ( wp_cache_add( $option_name, $lock_id, 'jetpack', $ttl ) ) {
220 + return $lock_id;
213 221 }
222 + return false; // Worker already active
214 223 }
215 224
216 - $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
225 + global $wpdb;
217 226
218 - if ( ! empty( $current_lock_value ) ) {
219 - // Check if time has passed to overwrite the lock - min 5s?
220 - if ( is_numeric( $current_lock_value ) && ( ( $current_microtime - $current_lock_value ) < self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
221 - // Still in previous lock, quit
222 - return false;
223 - }
224 -
225 - // If the value is not numeric (float/current time), we want to just overwrite it and continue.
227 + // 1) Check & clear expired lock (best effort; failure here is harmless)
228 + $expiry = (float) \Jetpack_Options::get_raw_option( $expires_name, 0 );
229 + if ( ! $expiry || $expiry < $now ) {
230 + // Either missing (edge case) or expired → clean up
231 + \Jetpack_Options::delete_raw_option( $option_name );
232 + \Jetpack_Options::delete_raw_option( $expires_name );
226 233 }
227 234
228 - // Update. We don't want it to autoload, as we want to fetch it right before the checks.
229 - \Jetpack_Options::update_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, false );
230 - // Give some time for the update to happen
231 - usleep( wp_rand( 1000, 3000 ) );
235 + // 2) Atomic acquisition: INSERT IGNORE (succeeds only if the lock doesn't exist)
236 + $inserted = $wpdb->query( // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching --- Ensure atomicity.
237 + $wpdb->prepare(
238 + "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload )
239 + VALUES ( %s, %s, 'no' )",
240 + $option_name,
241 + maybe_serialize( $lock_id )
242 + )
243 + );
232 244
233 - $updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
234 -
235 - if ( $updated_value === $current_microtime ) {
236 - return $current_microtime;
245 + if ( $inserted ) {
246 + // 3) We own the lock — store expiry separately
247 + \Jetpack_Options::update_raw_option( $expires_name, $now + $ttl, false );
248 + return $lock_id; // Success
237 249 }
238 250
251 + // Lock already present → normal state → do not spawn
239 252 return false;
240 253 }
241 254
242 255 /**
@@ -252,20 +265,26 @@
252 265 $lock_id = self::get_request_lock_id_from_request();
253 266 }
254 267
255 268 // If it's still not a valid lock_id, throw an error and let the lock process figure it out.
256 - if ( empty( $lock_id ) || ! is_numeric( $lock_id ) ) {
269 + if ( empty( $lock_id ) ) {
257 270 return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
258 271 }
259 272
260 273 if ( wp_using_ext_object_cache() ) {
261 - if ( (string) $lock_id === wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true ) ) {
274 + $cached = wp_cache_get( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack', true );
275 + if ( (string) $lock_id === $cached ) {
262 276 wp_cache_delete( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, 'jetpack' );
277 +
278 + return true;
263 279 }
280 +
281 + return false;
264 282 }
265 283
266 284 // If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
267 285 $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
286 +
268 287 if ( (string) $lock_id === $current_lock_value ) {
269 288 \Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
270 289 return true;
271 290 }
@@ -279,9 +298,9 @@
279 298 * @return array|string|string[]|null
280 299 */
281 300 public static function get_request_lock_id_from_request() {
282 301 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
283 - if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) || ! is_numeric( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
302 + if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
284 303 return null;
285 304 }
286 305
287 306 // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
@@ -291,9 +310,9 @@
291 310 /**
292 311 * Test Sync spawning functionality by making a request to the
293 312 * Sync spawning endpoint and storing the result (status code) in a transient.
294 313 *
295 - * @since $$next_version$$
314 + * @since 1.34.0
296 315 *
297 316 * @return bool True if we got a successful response, false otherwise.
298 317 */
299 318 public static function can_spawn_dedicated_sync_request() {
@@ -394,8 +413,15 @@
394 413 if ( $check_transient ) {
395 414 // Something happened and Dedicated Sync should not be automatically re-enabled.
396 415 return false;
397 416 }
417 + }
418 +
419 + $current_setting = Settings::is_dedicated_sync_enabled();
420 +
421 + // No need to update if current setting matches header value.
422 + if ( $current_setting === (bool) $dedicated_sync_enabled ) {
423 + return $current_setting;
398 424 }
399 425
400 426 Settings::update_settings(
401 427 array(