PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 12.3.2
Jetpack – WP Security, Backup, Speed, & Growth v12.3.2
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 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / jetpack_vendor / automattic / jetpack-sync / src / class-dedicated-sender.php
class-dedicated-sender.php
412 lines 14.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Dedicated Sender.
4 *
5 * The class is responsible for spawning dedicated Sync requests.
6 *
7 * @package automattic/jetpack-sync
8 */
9
10 namespace Automattic\Jetpack\Sync;
11
12 use WP_Error;
13 /**
14 * Class to manage Sync spawning.
15 * The purpose of this class is to provide the means to unblock Sync
16 * from running in the shutdown hook of regular requests by spawning a
17 * dedicated Sync request instead which will trigger Sync to run.
18 */
19 class Dedicated_Sender {
20
21 /**
22 * The transient name for storing the response code
23 * after spawning a dedicated sync test request.
24 */
25 const DEDICATED_SYNC_CHECK_TRANSIENT = 'jetpack_sync_dedicated_sync_spawn_check';
26
27 /**
28 * Validation string to check if the endpoint is working correctly.
29 *
30 * This is extracted and not hardcoded, as we might want to change it in the future.
31 */
32 const DEDICATED_SYNC_VALIDATION_STRING = 'DEDICATED SYNC OK';
33
34 /**
35 * Option name to use to keep the current request lock.
36 *
37 * The option format is `microtime(true)`.
38 */
39 const DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME = 'jetpack_sync_dedicated_spawn_lock';
40
41 /**
42 * What's the timeout for the request lock in seconds.
43 *
44 * 5 seconds as default value seems sane, but we might want to adjust that in the future.
45 */
46 const DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT = 5;
47
48 /**
49 * The query parameter name to use when passing the current lock id.
50 */
51 const DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME = 'request_lock_id';
52
53 /**
54 * The name of the transient to use to temporarily disable enabling of Dedicated sync.
55 */
56 const DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG = 'jetpack_sync_dedicated_sync_temp_disable';
57
58 /**
59 * Filter a URL to check if Dedicated Sync is enabled.
60 * We need to remove slashes and then run it through `urldecode` as sometimes the
61 * URL is in an encoded form, depending on server configuration.
62 *
63 * @param string $url The URL to filter.
64 *
65 * @return string
66 */
67 public static function prepare_url_for_dedicated_request_check( $url ) {
68 return urldecode( $url );
69 }
70 /**
71 * Check if this request should trigger Sync to run.
72 *
73 * @access public
74 *
75 * @return boolean True if this is a 'jetpack/v4/sync/spawn-sync', false otherwise.
76 */
77 public static function is_dedicated_sync_request() {
78 /**
79 * Check $_SERVER['REQUEST_URI'] first, to see if we're in the right context.
80 * This is done to make sure we can hook in very early in the initialization of WordPress to
81 * be able to send sync requests to the backend as fast as possible, without needing to continue
82 * loading things for the request.
83 */
84 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
85 return false;
86 }
87
88 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
89 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_SERVER['REQUEST_URI'] ) );
90 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
91 return true;
92 }
93
94 /**
95 * If the above check failed, we might have an issue with detecting calls to the REST endpoint early on.
96 * Sometimes, like when permalinks are disabled, the REST path is sent via the `rest_route` GET parameter.
97 * We want to check it too, to make sure we managed to cover more cases and be more certain we actually
98 * catch calls to the endpoint.
99 */
100 if ( ! isset( $_GET['rest_route'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
101 return false;
102 }
103
104 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
105 $check_url = self::prepare_url_for_dedicated_request_check( wp_unslash( $_GET['rest_route'] ) );
106 if ( strpos( $check_url, 'jetpack/v4/sync/spawn-sync' ) !== false ) {
107 return true;
108 }
109
110 return false;
111 }
112
113 /**
114 * Send a request to run Sync for a certain sync queue
115 * through HTTP request that doesn't halt page loading.
116 *
117 * @access public
118 *
119 * @param \Automattic\Jetpack\Sync\Queue $queue Queue object.
120 *
121 * @return boolean|WP_Error True if spawned, WP_Error otherwise.
122 */
123 public static function spawn_sync( $queue ) {
124 if ( ! Settings::is_dedicated_sync_enabled() ) {
125 return new WP_Error( 'dedicated_sync_disabled', 'Dedicated Sync flow is disabled.' );
126 }
127
128 if ( $queue->is_locked() ) {
129 return new WP_Error( 'locked_queue_' . $queue->id );
130 }
131
132 if ( $queue->size() === 0 ) {
133 return new WP_Error( 'empty_queue_' . $queue->id );
134 }
135
136 // Return early if we've gotten a retry-after header response that is not expired.
137 $retry_time = get_option( Actions::RETRY_AFTER_PREFIX . $queue->id );
138 if ( $retry_time && $retry_time >= microtime( true ) ) {
139 return new WP_Error( 'retry_after_' . $queue->id );
140 }
141
142 // Don't sync if we are throttled.
143 $sync_next_time = Sender::get_instance()->get_next_sync_time( $queue->id );
144 if ( $sync_next_time > microtime( true ) ) {
145 return new WP_Error( 'sync_throttled_' . $queue->id );
146 }
147 /**
148 * How much time to wait before we start suspecting Dedicated Sync is in trouble.
149 */
150 $queue_send_time_threshold = 30 * MINUTE_IN_SECONDS;
151
152 $queue_lag = $queue->lag();
153
154 // Only check if we're failing to send events if the queue lag is longer than the threshold.
155 if ( $queue_lag > $queue_send_time_threshold ) {
156 /**
157 * Check if Dedicated Sync is healthy and revert to Default Sync if such case is detected.
158 */
159 $last_successful_queue_send_time = get_option( Actions::LAST_SUCCESS_PREFIX . $queue->id, null );
160
161 if ( $last_successful_queue_send_time === null ) {
162 /**
163 * No successful sync sending completed. This might be either a "new" sync site or a site that's totally stuck.
164 */
165 self::on_dedicated_sync_lag_not_sending_threshold_reached();
166
167 return new WP_Error( 'dedicated_sync_not_sending', 'Dedicated Sync is not successfully sending events' );
168 } else {
169 /**
170 * We have recorded a successful sending of events. Let's see if that is not too long ago in the past.
171 */
172 $time_since_last_succesful_send = time() - $last_successful_queue_send_time;
173
174 if ( $time_since_last_succesful_send > $queue_send_time_threshold ) {
175 // We haven't successfully sent stuff in more than 30 minutes. Revert to Default Sync
176 self::on_dedicated_sync_lag_not_sending_threshold_reached();
177
178 return new WP_Error( 'dedicated_sync_not_sending', 'Dedicated Sync is not successfully sending events' );
179 }
180 }
181 }
182
183 /**
184 * Try to acquire a request lock, so we don't spawn multiple requests at the same time.
185 * This should prevent cases where sites might have limits on the amount of simultaneous requests.
186 */
187 $request_lock = self::try_lock_spawn_request();
188 if ( ! $request_lock ) {
189 return new WP_Error( 'dedicated_request_lock', 'Unable to acquire request lock' );
190 }
191
192 $url = rest_url( 'jetpack/v4/sync/spawn-sync' );
193 $url = add_query_arg( 'time', time(), $url ); // Enforce Cache busting.
194 $url = add_query_arg( self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME, $request_lock, $url );
195
196 $args = array(
197 'cookies' => $_COOKIE,
198 'blocking' => false,
199 'timeout' => 0.01,
200 /** This filter is documented in wp-includes/class-wp-http-streams.php */
201 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
202 );
203
204 $result = wp_remote_get( $url, $args );
205 if ( is_wp_error( $result ) ) {
206 return $result;
207 }
208
209 return true;
210 }
211
212 /**
213 * Attempt to acquire a request lock.
214 *
215 * To avoid spawning multiple requests at the same time, we need to have a quick lock that will
216 * allow only a single request to continue if we try to spawn multiple at the same time.
217 *
218 * @return false|mixed|string
219 */
220 public static function try_lock_spawn_request() {
221 $current_microtime = (string) microtime( true );
222
223 $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
224
225 if ( ! empty( $current_lock_value ) ) {
226 // Check if time has passed to overwrite the lock - min 5s?
227 if ( is_numeric( $current_lock_value ) && ( ( $current_microtime - $current_lock_value ) < self::DEDICATED_SYNC_REQUEST_LOCK_TIMEOUT ) ) {
228 // Still in previous lock, quit
229 return false;
230 }
231
232 // If the value is not numeric (float/current time), we want to just overwrite it and continue.
233 }
234
235 // Update. We don't want it to autoload, as we want to fetch it right before the checks.
236 \Jetpack_Options::update_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, $current_microtime, false );
237 // Give some time for the update to happen
238 usleep( wp_rand( 1000, 3000 ) );
239
240 $updated_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
241
242 if ( $updated_value === $current_microtime ) {
243 return $current_microtime;
244 }
245
246 return false;
247 }
248
249 /**
250 * Attempt to release the request lock.
251 *
252 * @param string $lock_id The request lock that's currently being held.
253 *
254 * @return bool|WP_Error
255 */
256 public static function try_release_lock_spawn_request( $lock_id = '' ) {
257 // Try to get the lock_id from the current request if it's not supplied.
258 if ( empty( $lock_id ) ) {
259 $lock_id = self::get_request_lock_id_from_request();
260 }
261
262 // If it's still not a valid lock_id, throw an error and let the lock process figure it out.
263 if ( empty( $lock_id ) || ! is_numeric( $lock_id ) ) {
264 return new WP_Error( 'dedicated_request_lock_invalid', 'Invalid lock_id supplied for unlock' );
265 }
266
267 $current_lock_value = \Jetpack_Options::get_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME, null );
268
269 // If this is the flow that has the lock, let's release it so we can spawn other requests afterwards
270 if ( (string) $lock_id === $current_lock_value ) {
271 \Jetpack_Options::delete_raw_option( self::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME );
272 return true;
273 }
274
275 return false;
276 }
277
278 /**
279 * Try to get the request lock id from the current request.
280 *
281 * @return array|string|string[]|null
282 */
283 public static function get_request_lock_id_from_request() {
284 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
285 if ( ! isset( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) || ! is_numeric( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] ) ) {
286 return null;
287 }
288
289 // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
290 return wp_unslash( $_GET[ self::DEDICATED_SYNC_REQUEST_LOCK_QUERY_PARAM_NAME ] );
291 }
292
293 /**
294 * Test Sync spawning functionality by making a request to the
295 * Sync spawning endpoint and storing the result (status code) in a transient.
296 *
297 * @since $$next_version$$
298 *
299 * @return bool True if we got a successful response, false otherwise.
300 */
301 public static function can_spawn_dedicated_sync_request() {
302 $dedicated_sync_check_transient = self::DEDICATED_SYNC_CHECK_TRANSIENT;
303
304 $dedicated_sync_response_body = get_transient( $dedicated_sync_check_transient );
305
306 if ( false === $dedicated_sync_response_body ) {
307 $url = rest_url( 'jetpack/v4/sync/spawn-sync' );
308 $url = add_query_arg( 'time', time(), $url ); // Enforce Cache busting.
309 $args = array(
310 'cookies' => $_COOKIE,
311 'timeout' => 30,
312 /** This filter is documented in wp-includes/class-wp-http-streams.php */
313 'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
314 );
315
316 $response = wp_remote_get( $url, $args );
317 $dedicated_sync_response_code = wp_remote_retrieve_response_code( $response );
318 $dedicated_sync_response_body = trim( wp_remote_retrieve_body( $response ) );
319
320 /**
321 * Limit the size of the body that we save in the transient to avoid cases where an error
322 * occurs and a whole generated HTML page is returned. We don't need to store the whole thing.
323 *
324 * The regexp check is done to make sure we can detect the string even if the body returns some additional
325 * output, like some caching plugins do when they try to pad the request.
326 */
327 $regexp = '!' . preg_quote( self::DEDICATED_SYNC_VALIDATION_STRING, '!' ) . '!uis';
328 if ( preg_match( $regexp, $dedicated_sync_response_body ) ) {
329 $saved_response_body = self::DEDICATED_SYNC_VALIDATION_STRING;
330 } else {
331 $saved_response_body = time();
332 }
333
334 set_transient( $dedicated_sync_check_transient, $saved_response_body, HOUR_IN_SECONDS );
335
336 // Send a bit more information to WordPress.com to help debugging issues.
337 if ( $saved_response_body !== self::DEDICATED_SYNC_VALIDATION_STRING ) {
338 $data = array(
339 'timestamp' => microtime( true ),
340 'response_code' => $dedicated_sync_response_code,
341 'response_body' => $dedicated_sync_response_body,
342
343 // Send the flow type that was attempted.
344 'sync_flow_type' => 'dedicated',
345 );
346
347 $sender = Sender::get_instance();
348
349 $sender->send_action( 'jetpack_sync_flow_error_enable', $data );
350 }
351 }
352
353 return self::DEDICATED_SYNC_VALIDATION_STRING === $dedicated_sync_response_body;
354 }
355
356 /**
357 * Disable dedicated sync and set a transient to prevent re-enabling it for some time.
358 *
359 * @return void
360 */
361 public static function on_dedicated_sync_lag_not_sending_threshold_reached() {
362 set_transient( self::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG, true, 6 * HOUR_IN_SECONDS );
363
364 Settings::update_settings(
365 array(
366 'dedicated_sync_enabled' => 0,
367 )
368 );
369
370 // Inform that we had to temporarily disable Dedicated Sync
371 $data = array(
372 'timestamp' => microtime( true ),
373
374 // Send the flow type that was attempted.
375 'sync_flow_type' => 'dedicated',
376 );
377
378 $sender = Sender::get_instance();
379
380 $sender->send_action( 'jetpack_sync_flow_error_temp_disable', $data );
381 }
382
383 /**
384 * Disable or enable Dedicated Sync sender based on the header value returned from WordPress.com
385 *
386 * @param string $dedicated_sync_header The Dedicated Sync header value - `on` or `off`.
387 *
388 * @return bool Whether Dedicated Sync is going to be enabled or not.
389 */
390 public static function maybe_change_dedicated_sync_status_from_wpcom_header( $dedicated_sync_header ) {
391 $dedicated_sync_enabled = 'on' === $dedicated_sync_header ? 1 : 0;
392
393 // Prevent enabling of Dedicated sync via header flag if we're in an autoheal timeout.
394 if ( $dedicated_sync_enabled ) {
395 $check_transient = get_transient( self::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG );
396
397 if ( $check_transient ) {
398 // Something happened and Dedicated Sync should not be automatically re-enabled.
399 return false;
400 }
401 }
402
403 Settings::update_settings(
404 array(
405 'dedicated_sync_enabled' => $dedicated_sync_enabled,
406 )
407 );
408
409 return Settings::is_dedicated_sync_enabled();
410 }
411 }
412