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-sync / src / class-actions.php

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

1,296 lines 39.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A class that defines syncable actions for Jetpack.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8 namespace Automattic\Jetpack\Sync;
9
10 use Automattic\Jetpack\Connection\Client;
11 use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
12 use Automattic\Jetpack\Constants;
13 use Automattic\Jetpack\Identity_Crisis;
14 use Automattic\Jetpack\Status;
15 use Automattic\Jetpack\Sync\Modules\WooCommerce_HPOS_Orders;
16 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
17 use WP_Error;
18
19 /**
20 * The role of this class is to hook the Sync subsystem into WordPress - when to listen for actions,
21 * when to send, when to perform a full sync, etc.
22 *
23 * It also binds the action to send data to WPCOM to Jetpack's XMLRPC client object.
24 */
25 class Actions {
26
27 /**
28 * Name of the retry-after option prefix.
29 *
30 * @access public
31 *
32 * @var string
33 */
34 const RETRY_AFTER_PREFIX = 'jp_sync_retry_after_';
35
36 /**
37 * Name of the error log option prefix.
38 *
39 * @access public
40 *
41 * @var string
42 */
43 const ERROR_LOG_PREFIX = 'jp_sync_error_log_';
44
45 /**
46 * Name of the last successful sync option prefix.
47 *
48 * @access public
49 *
50 * @var string
51 */
52 const LAST_SUCCESS_PREFIX = 'jp_sync_last_success_';
53
54 /**
55 * A variable to hold a sync sender object.
56 *
57 * @access public
58 * @static
59 *
60 * @var \Automattic\Jetpack\Sync\Sender
61 */
62 public static $sender = null;
63
64 /**
65 * A variable to hold a sync listener object.
66 *
67 * @access public
68 * @static
69 *
70 * @var \Automattic\Jetpack\Sync\Listener
71 */
72 public static $listener = null;
73
74 /**
75 * Name of the sync cron schedule.
76 *
77 * @access public
78 *
79 * @var string
80 */
81 const DEFAULT_SYNC_CRON_INTERVAL_NAME = 'jetpack_sync_interval';
82
83 /**
84 * Interval between the last and the next sync cron action.
85 *
86 * @access public
87 *
88 * @var int
89 */
90 const DEFAULT_SYNC_CRON_INTERVAL_VALUE = 300; // 5 * MINUTE_IN_SECONDS;
91
92 /**
93 * Initialize Sync for cron jobs, set up listeners for WordPress Actions,
94 * and set up a shut-down action for sending actions to WordPress.com
95 * If dedicated Sync is enabled and this is a dedicated Sync request
96 * up an init action for sending actions to WordPress.com instead.
97 *
98 * @access public
99 * @static
100 */
101 public static function init() {
102 // Everything below this point should only happen if we're a valid sync site.
103 if ( ! self::sync_allowed() ) {
104 return;
105 }
106
107 // If dedicated Sync is enabled and this is a dedicated Sync request, no need to
108 // initialize Sync for cron jobs or set up a shut-down action for sending actions to WordPress.com.
109 // We only need to set up an init action for sending actions to WordPress.com and exit early.
110 // Note: We also need to initialize the listener so that callable and constant changes, eg actions that
111 // rely on 'jetpack_sync_before_send_queue_sync' are picked up and added to the queue if needed.
112 if ( Settings::is_dedicated_sync_enabled() && Dedicated_Sender::is_dedicated_sync_request() ) {
113 self::initialize_listener();
114 add_action( 'init', array( __CLASS__, 'add_dedicated_sync_sender_init' ), 200 );
115 return;
116 }
117
118 if ( self::sync_via_cron_allowed() ) {
119 add_action( 'init', array( __CLASS__, 'init_sync_cron_jobs' ), 1 );
120 } elseif ( wp_next_scheduled( 'jetpack_sync_cron' ) ) {
121 self::clear_sync_cron_jobs();
122 }
123 // When importing via cron, do not sync.
124 add_action( 'wp_cron_importer_hook', array( __CLASS__, 'set_is_importing_true' ), 1 );
125
126 // Sync connected user role changes to WordPress.com.
127 Users::init();
128
129 // Publicize filter to prevent publicizing blacklisted post types.
130 add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 );
131
132 /**
133 * Fires on every request before default loading sync listener code.
134 * Return false to not load sync listener code that monitors common
135 * WP actions to be serialized.
136 *
137 * By default this returns true for cron jobs, non-GET-requests, or requests where the
138 * user is logged-in.
139 *
140 * @since 1.6.3
141 * @since-jetpack 4.2.0
142 *
143 * @param bool should we load sync listener code for this request
144 */
145 if ( apply_filters( 'jetpack_sync_listener_should_load', true ) ) {
146 self::initialize_listener();
147 }
148
149 add_action( 'init', array( __CLASS__, 'add_sender_shutdown' ), 90 );
150 }
151
152 /**
153 * Prepares sync to send actions on shutdown for the current request.
154 *
155 * @access public
156 * @static
157 */
158 public static function add_sender_shutdown() {
159 /**
160 * Fires on every request before default loading sync sender code.
161 * Return false to not load sync sender code that serializes pending
162 * data and sends it to WPCOM for processing.
163 *
164 * By default this returns true for cron jobs, POST requests, admin requests, or requests
165 * by users who can manage_options.
166 *
167 * @since 1.6.3
168 * @since-jetpack 4.2.0
169 *
170 * @param bool should we load sync sender code for this request
171 */
172 if ( apply_filters(
173 'jetpack_sync_sender_should_load',
174 self::should_initialize_sender()
175 ) ) {
176 self::initialize_sender();
177 add_action( 'shutdown', array( self::$sender, 'do_sync' ), 9998 );
178
179 if ( self::should_initialize_sender( true ) ) {
180 add_action( 'shutdown', array( self::$sender, 'do_full_sync' ), 9999 );
181 }
182 }
183 }
184
185 /**
186 * Immediately sends actions on init for the current dedicated Sync request.
187 *
188 * @access public
189 * @static
190 */
191 public static function add_dedicated_sync_sender_init() {
192 if ( apply_filters(
193 'jetpack_sync_sender_should_load',
194 true
195 ) ) {
196 self::initialize_sender();
197 self::$sender->do_dedicated_sync_and_exit();
198 }
199 }
200
201 /**
202 * Define JETPACK_SYNC_READ_ONLY constant if not defined.
203 * This notifies sync to not run in shutdown if it was initialized during init.
204 *
205 * @access public
206 * @static
207 */
208 public static function mark_sync_read_only() {
209 Constants::set_constant( 'JETPACK_SYNC_READ_ONLY', true );
210 }
211
212 /**
213 * Decides if the sender should run on shutdown for this request.
214 *
215 * @access public
216 * @static
217 *
218 * @param bool $full_sync Whether the Full Sync sender should run on shutdown for this request.
219 *
220 * @return bool
221 */
222 public static function should_initialize_sender( $full_sync = false ) {
223
224 // Allow for explicit disable of Sync from request param jetpack_sync_read_only.
225 if ( isset( $_REQUEST['jetpack_sync_read_only'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
226 self::mark_sync_read_only();
227 return false;
228 }
229
230 if ( Constants::is_true( 'DOING_CRON' ) ) {
231 return self::sync_via_cron_allowed();
232 }
233
234 /**
235 * For now, if dedicated Sync is enabled we will always initialize send, even for GET and unauthenticated requests
236 * but not for Full Sync, since it will still happen on shutdown.
237 */
238 if ( false === $full_sync && Settings::is_dedicated_sync_enabled() ) {
239 return true;
240 }
241
242 if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
243 return true;
244 }
245
246 if ( current_user_can( 'manage_options' ) ) {
247 return true;
248 }
249
250 if ( is_admin() ) {
251 return true;
252 }
253
254 if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
255 return true;
256 }
257
258 if ( Constants::get_constant( 'WP_CLI' ) ) {
259 return true;
260 }
261
262 return false;
263 }
264
265 /**
266 * Decides if the sender should run on shutdown when actions are queued.
267 *
268 * @access public
269 * @static
270 *
271 * @param bool $enable Should we initilize sender.
272 * @return bool
273 */
274 public static function should_initialize_sender_enqueue( $enable ) {
275
276 // If $enabled is false don't modify it, only check cron if enabled.
277 if ( false === $enable ) {
278 return $enable;
279 }
280
281 if ( Constants::is_true( 'DOING_CRON' ) ) {
282 return self::sync_via_cron_allowed();
283 }
284
285 return true;
286 }
287
288 /**
289 * Decides if sync should run at all during this request.
290 *
291 * @access public
292 * @static
293 *
294 * @return bool
295 */
296 public static function sync_allowed() {
297 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
298 return false;
299 }
300
301 if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
302 return true;
303 }
304
305 if ( ! Settings::is_sync_enabled() ) {
306 return false;
307 }
308
309 if ( ( new Status() )->is_offline_mode() ) {
310 return false;
311 }
312
313 $connection = new Jetpack_Connection();
314 if ( ! $connection->is_connected() ) {
315 if ( ! doing_action( 'jetpack_site_registered' ) ) {
316 return false;
317 }
318 }
319
320 // By now, we know the site is connected, so we can return false if in safe mode.
321 if ( ( new Status() )->in_safe_mode() ) {
322 return false;
323 }
324
325 return true;
326 }
327
328 /**
329 * Helper function to get details as to why sync is not allowed, if it is not allowed.
330 *
331 * @return array
332 */
333 public static function get_debug_details() {
334 $debug = array();
335 $debug['debug_details']['sync_allowed'] = self::sync_allowed();
336 $debug['debug_details']['sync_health'] = Health::get_status();
337 if ( false === $debug['debug_details']['sync_allowed'] ) {
338 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
339 $debug['debug_details']['is_wpcom'] = true;
340 }
341 if ( defined( 'PHPUNIT_JETPACK_TESTSUITE' ) ) {
342 $debug['debug_details']['PHPUNIT_JETPACK_TESTSUITE'] = true;
343 }
344 if ( ! Settings::is_sync_enabled() ) {
345 $debug['debug_details']['is_sync_enabled'] = false;
346 $debug['debug_details']['jetpack_sync_disable'] = Settings::get_setting( 'disable' );
347 $debug['debug_details']['jetpack_sync_network_disable'] = Settings::get_setting( 'network_disable' );
348 }
349 if ( ( new Status() )->is_offline_mode() ) {
350 $debug['debug_details']['is_offline_mode'] = true;
351 }
352 if ( ( new Status() )->in_safe_mode() ) {
353 $debug['debug_details']['in_safe_mode'] = true;
354 }
355 $connection = new Jetpack_Connection();
356 if ( ! $connection->is_connected() ) {
357 $debug['debug_details']['active_connection'] = false;
358 }
359 }
360
361 // Sync locks.
362 $debug['debug_details']['dedicated_sync_enabled'] = Settings::is_dedicated_sync_enabled();
363
364 $queue = self::$sender->get_sync_queue();
365 $full_queue = self::$sender->get_full_sync_queue();
366 // We are sending the expiry vs the actual dedicated lock value to ensure backwards compatibility
367 // with previous versions where the lock value was a timestamp.
368 $dedicated_sync_lock_option_name = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
369 $dedicated_sync_lock_expires_name = $dedicated_sync_lock_option_name . '_expires';
370
371 $debug['debug_details']['sync_locks'] = array(
372 'retry_time_sync' => get_option( self::RETRY_AFTER_PREFIX . 'sync' ),
373 'retry_time_full_sync' => get_option( self::RETRY_AFTER_PREFIX . 'full_sync' ),
374 'next_sync_time_sync' => self::$sender->get_next_sync_time( 'sync' ),
375 'next_sync_time_full_sync' => self::$sender->get_next_sync_time( 'full_sync' ),
376 'queue_locked_sync' => $queue->is_locked(),
377 'queue_locked_full_sync' => $full_queue->is_locked(),
378 'dedicated_sync_request_lock' => \Jetpack_Options::get_raw_option( $dedicated_sync_lock_expires_name, null ),
379 'dedicated_sync_temporary_disable_flag' => get_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG ),
380 );
381
382 // Sync Logs.
383 $debug['debug_details']['last_succesful_sync'] = get_option( self::LAST_SUCCESS_PREFIX . 'sync', '' );
384 $debug['debug_details']['sync_error_log'] = get_option( self::ERROR_LOG_PREFIX . 'sync', '' );
385
386 return $debug;
387 }
388
389 /**
390 * Determines if syncing during a cron job is allowed.
391 *
392 * @access public
393 * @static
394 *
395 * @return bool|int
396 */
397 public static function sync_via_cron_allowed() {
398 return ( Settings::get_setting( 'sync_via_cron' ) );
399 }
400
401 /**
402 * Decides if the given post should be Publicized based on its type.
403 *
404 * @access public
405 * @static
406 *
407 * @param bool $should_publicize Publicize status prior to this filter running.
408 * @param \WP_Post $post The post to test for Publicizability.
409 * @return bool
410 */
411 public static function prevent_publicize_blacklisted_posts( $should_publicize, $post ) {
412 if ( in_array( $post->post_type, Settings::get_setting( 'post_types_blacklist' ), true ) ) {
413 return false;
414 }
415
416 return $should_publicize;
417 }
418
419 /**
420 * Set an importing flag to `true` in sync settings.
421 *
422 * @access public
423 * @static
424 */
425 public static function set_is_importing_true() {
426 Settings::set_importing( true );
427 }
428
429 /**
430 * Sends data to WordPress.com via an XMLRPC or a REST API request based on the settings.
431 *
432 * @access public
433 * @static
434 *
435 * @param object $data Data relating to a sync action.
436 * @param string $codec_name The name of the codec that encodes the data.
437 * @param float $sent_timestamp Current server time so we can compensate for clock differences.
438 * @param string $queue_id The queue the action belongs to, sync or full_sync.
439 * @param float $checkout_duration Time spent retrieving queue items from the DB.
440 * @param float $preprocess_duration Time spent converting queue items into data to send.
441 * @param int $queue_size The size of the sync queue at the time of processing.
442 * @param string $buffer_id The ID of the Queue buffer checked out for processing.
443 * @return mixed|WP_Error The result of the sending request.
444 */
445 public static function send_data( $data, $codec_name, $sent_timestamp, $queue_id, $checkout_duration, $preprocess_duration, $queue_size = null, $buffer_id = null ) {
446
447 $query_args = array(
448
449 'sync' => '1', // Add an extra parameter to the URL so we can tell it's a sync action.
450 'codec' => $codec_name,
451 'timestamp' => $sent_timestamp,
452 'queue' => $queue_id,
453 'cd' => sprintf( '%.4f', $checkout_duration ),
454 'pd' => sprintf( '%.4f', $preprocess_duration ),
455 'queue_size' => $queue_size,
456 'buffer_id' => $buffer_id,
457 // TODO this will be extended in the future. Might be good to extract in a separate method to support future entries too.
458 'sync_flow_type' => Settings::is_dedicated_sync_enabled() ? 'dedicated' : 'default',
459 'storage_type' => Settings::is_custom_queue_table_enabled() ? 'custom' : 'options',
460 );
461
462 $query_args['timeout'] = Settings::is_doing_cron() ? 30 : 20;
463
464 if ( 'immediate-send' === $queue_id ) {
465 $query_args['timeout'] = 30;
466 }
467
468 /**
469 * Filters query parameters appended to the Sync request URL sent to WordPress.com.
470 *
471 * @since 1.6.3
472 * @since-jetpack 4.7.0
473 *
474 * @param array $query_args associative array of query parameters.
475 */
476 $query_args = apply_filters( 'jetpack_sync_send_data_query_args', $query_args );
477
478 $retry_after_header = false;
479 $dedicated_sync_header = false;
480
481 // If REST API is enabled, use it.
482 if ( Settings::is_wpcom_rest_api_enabled() ) {
483 $jsonl_data = self::prepare_jsonl_data( $data );
484 $url = '/sites/' . \Jetpack_Options::get_option( 'id' ) . '/jetpack-sync-actions';
485 $url = add_query_arg( $query_args, $url );
486 $args = array(
487 'method' => 'POST',
488 'format' => 'jsonl',
489 'timeout' => $query_args['timeout'],
490 );
491
492 $response = Client::wpcom_json_api_request_as_blog( $url, '2', $args, $jsonl_data, 'wpcom' );
493 $retry_after_header = wp_remote_retrieve_header( $response, 'Retry-After' ) ? wp_remote_retrieve_header( $response, 'Retry-After' ) : false;
494 $dedicated_sync_header = wp_remote_retrieve_header( $response, 'Jetpack-Dedicated-Sync' ) ? wp_remote_retrieve_header( $response, 'Jetpack-Dedicated-Sync' ) : false;
495 $response = self::process_rest_api_response( $response );
496 } else { // Use XML-RPC.
497 $connection = new Jetpack_Connection();
498 $url = add_query_arg( $query_args, $connection->xmlrpc_api_url() );
499
500 // If we're currently updating to Jetpack 7.7, the IXR client may be missing briefly
501 // because since 7.7 it's being autoloaded with Composer.
502 if ( ! class_exists( '\\Jetpack_IXR_Client' ) ) {
503 return new WP_Error(
504 'ixr_client_missing',
505 esc_html__( 'Sync has been aborted because the IXR client is missing.', 'jetpack-sync' )
506 );
507 }
508
509 $rpc = new \Jetpack_IXR_Client(
510 array(
511 'url' => $url,
512 'timeout' => $query_args['timeout'],
513 )
514 );
515 $result = $rpc->query( 'jetpack.syncActions', $data );
516 $retry_after_header = $rpc->get_response_header( 'Retry-After' );
517 $dedicated_sync_header = $rpc->get_response_header( 'Jetpack-Dedicated-Sync' );
518 if ( $result ) {
519 $response = $rpc->getResponse();
520 } else {
521 $response = $rpc->get_jetpack_error();
522 }
523 }
524
525 // Adhere to Retry-After headers.
526 if ( false !== $retry_after_header ) {
527 if ( (int) $retry_after_header > 0 ) {
528 update_option( self::RETRY_AFTER_PREFIX . $queue_id, microtime( true ) + (int) $retry_after_header, false );
529 } else {
530 // if unexpected value default to 3 minutes.
531 update_option( self::RETRY_AFTER_PREFIX . $queue_id, microtime( true ) + 180, false );
532 }
533 }
534
535 // Enable/Disable Dedicated Sync flow via response headers.
536 if ( false !== $dedicated_sync_header ) {
537 Dedicated_Sender::maybe_change_dedicated_sync_status_from_wpcom_header( $dedicated_sync_header );
538 }
539
540 if ( is_wp_error( $response ) ) {
541 $error = $response;
542 if ( false === $retry_after_header ) {
543 // We received a non standard response from WP.com, lets backoff from sending requests for 1 minute.
544 update_option( self::RETRY_AFTER_PREFIX . $queue_id, microtime( true ) + 60, false );
545 }
546 // Record Sync Errors.
547 $error_log = get_option( self::ERROR_LOG_PREFIX . $queue_id, array() );
548 if ( ! is_array( $error_log ) ) {
549 $error_log = array();
550 }
551 // Trim existing array to last 4 entries.
552 if ( 5 <= count( $error_log ) ) {
553 $error_log = array_slice( $error_log, -4, null, true );
554 }
555 // Add new error indexed to time.
556 if ( isset( $rpc ) && ! empty( $rpc->get_last_response() ) ) {
557 $error_with_last_response = clone $error;
558 $error_with_last_response->add_data( $rpc->get_last_response() );
559 $error_log[ (string) microtime( true ) ] = $error_with_last_response;
560 } else {
561 $error_log[ (string) microtime( true ) ] = $error;
562 }
563
564 // Update the error log.
565 update_option( self::ERROR_LOG_PREFIX . $queue_id, $error_log );
566 return $error;
567 }
568
569 // Check if WordPress.com IDC mitigation blocked the sync request.
570 if ( Identity_Crisis::init()->check_response_for_idc( $response ) ) {
571 return new WP_Error(
572 'sync_error_idc',
573 esc_html__( 'Sync has been blocked from WordPress.com because it would cause an identity crisis', 'jetpack-sync' )
574 );
575 }
576
577 if ( isset( $response['processed_items'] ) ) { // Return only processed items.
578 $response = $response['processed_items'];
579 }
580
581 // Record last successful sync.
582 update_option( self::LAST_SUCCESS_PREFIX . $queue_id, microtime( true ), false );
583
584 return $response;
585 }
586
587 /**
588 * Kicks off the initial sync.
589 *
590 * @access public
591 * @static
592 *
593 * @return bool|null False if sync is not allowed.
594 */
595 public static function do_initial_sync() {
596 // Let's not sync if we are not supposed to.
597 if ( ! self::sync_allowed() ) {
598 return false;
599 }
600
601 // Don't start new sync if a full sync is in process.
602 $full_sync_module = Modules::get_module( 'full-sync' );
603 '@phan-var Modules\Full_Sync_Immediately|Modules\Full_Sync $full_sync_module';
604 if ( $full_sync_module && $full_sync_module->is_started() && ! $full_sync_module->is_finished() ) {
605 return false;
606 }
607
608 $initial_sync_config = array(
609 'options' => true,
610 'functions' => true,
611 'constants' => true,
612 'users' => array( get_current_user_id() ),
613 'network_options' => true,
614 );
615
616 self::do_full_sync( $initial_sync_config, 'initial_sync' );
617 }
618
619 /**
620 * Do an initial full sync only if one has not already been started.
621 *
622 * @return bool|null False if the initial full sync was already started, otherwise null.
623 */
624 public static function do_only_first_initial_sync() {
625 $full_sync_module = Modules::get_module( 'full-sync' );
626 '@phan-var Modules\Full_Sync_Immediately|Modules\Full_Sync $full_sync_module';
627 if ( $full_sync_module && $full_sync_module->is_started() ) {
628 return false;
629 }
630
631 static::do_initial_sync();
632 }
633
634 /**
635 * Kicks off a full sync.
636 *
637 * @access public
638 * @static
639 *
640 * @param array $modules The sync modules should be included in this full sync. All will be included if null.
641 * @param mixed $context The context where the full sync was initiated from.
642 * @return bool True if full sync was successfully started.
643 */
644 public static function do_full_sync( $modules = null, $context = null ) {
645 if ( ! self::sync_allowed() ) {
646 return false;
647 }
648
649 $full_sync_module = Modules::get_module( 'full-sync' );
650 '@phan-var Modules\Full_Sync_Immediately|Modules\Full_Sync $full_sync_module';
651
652 if ( ! $full_sync_module ) {
653 return false;
654 }
655
656 self::initialize_listener();
657
658 $full_sync_module->start( $modules, $context );
659
660 return true;
661 }
662
663 /**
664 * Adds a cron schedule for regular syncing via cron, unless the schedule already exists.
665 *
666 * @access public
667 * @static
668 *
669 * @param array $schedules The list of WordPress cron schedules prior to this filter.
670 * @return array A list of WordPress cron schedules with the Jetpack sync interval added.
671 */
672 public static function jetpack_cron_schedule( $schedules ) {
673 if ( ! isset( $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] ) ) {
674 $minutes = ( self::DEFAULT_SYNC_CRON_INTERVAL_VALUE / 60 );
675 /* translators: %d is an integer indicating the number of minutes. */
676 $display = sprintf( __( 'Every %d minutes', 'jetpack-sync' ), $minutes );
677 $schedules[ self::DEFAULT_SYNC_CRON_INTERVAL_NAME ] = array(
678 'interval' => self::DEFAULT_SYNC_CRON_INTERVAL_VALUE,
679 'display' => $display,
680 );
681 }
682 return $schedules;
683 }
684
685 /**
686 * Starts an incremental sync via cron.
687 *
688 * @access public
689 * @static
690 */
691 public static function do_cron_sync() {
692 if ( ! self::sync_allowed() ) {
693 return;
694 }
695
696 self::initialize_sender();
697
698 $time_limit = Settings::get_setting( 'cron_sync_time_limit' );
699 $start_time = time();
700 $executions = 0;
701
702 $lock_id = Dedicated_Sender::try_lock_spawn_request();
703
704 do {
705 $next_sync_time = self::$sender->get_next_sync_time( 'sync' );
706
707 if ( $next_sync_time ) {
708 $delay = $next_sync_time - time() + 1;
709 if ( $delay > 15 ) {
710 break;
711 } elseif ( $delay > 0 ) {
712 sleep( (int) $delay );
713 }
714 }
715
716 $result = self::$sender->do_sync_and_set_delays( self::$sender->get_sync_queue() );
717
718 if ( is_wp_error( $result ) && in_array( $result->get_error_code(), array( 'unclosed_buffer', 'sync_throttled' ), true ) ) {
719 $result = true; // Give it some time.
720 }
721 // # of send actions performed.
722 ++$executions;
723
724 } while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
725
726 if ( $lock_id ) {
727 Dedicated_Sender::try_release_lock_spawn_request( $lock_id );
728 }
729
730 return $executions;
731 }
732
733 /**
734 * Starts a full sync via cron.
735 *
736 * @access public
737 * @static
738 */
739 public static function do_cron_full_sync() {
740 if ( ! self::sync_allowed() ) {
741 return;
742 }
743
744 self::initialize_sender();
745
746 $executions = 0;
747
748 $next_sync_time = self::$sender->get_next_sync_time( 'full_sync' );
749
750 if ( $next_sync_time ) {
751 $delay = $next_sync_time - time() + 1;
752 if ( $delay > 15 ) {
753 return;
754 } elseif ( $delay > 0 ) {
755 sleep( (int) $delay );
756 }
757 }
758
759 // Explicitly only allow 1 do_full_sync call until issue with Immediate Full Sync is resolved.
760 // For more context see p1HpG7-9pe-p2.
761 self::$sender->do_full_sync();
762 ++$executions;
763
764 return $executions;
765 }
766
767 /**
768 * Try to send actions until we run out of things to send,
769 * or have to wait more than 15s before sending again,
770 * or we hit a lock or some other sending issue
771 *
772 * @access public
773 * @static
774 *
775 * @param string $type Sync type. Can be `sync` or `full_sync`.
776 */
777 public static function do_cron_sync_by_type( $type ) {
778 if ( ! self::sync_allowed() || ( 'sync' !== $type && 'full_sync' !== $type ) ) {
779 return;
780 }
781
782 self::initialize_sender();
783
784 $time_limit = Settings::get_setting( 'cron_sync_time_limit' );
785 $start_time = time();
786 $executions = 0;
787
788 do {
789 $next_sync_time = self::$sender->get_next_sync_time( $type );
790
791 if ( $next_sync_time ) {
792 $delay = $next_sync_time - time() + 1;
793 if ( $delay > 15 ) {
794 break;
795 } elseif ( $delay > 0 ) {
796 sleep( (int) $delay );
797 }
798 }
799
800 // Explicitly only allow 1 do_full_sync call until issue with Immediate Full Sync is resolved.
801 // For more context see p1HpG7-9pe-p2.
802 if ( 'full_sync' === $type && $executions >= 1 ) {
803 break;
804 }
805
806 /**
807 * Only try to sync once if Dedicated Sync is enabled. Dedicated Sync has its own requeueing mechanism
808 * that will re-run it if there are items in the queue at the end.
809 */
810 if ( 'sync' === $type && $executions >= 1 && Settings::is_dedicated_sync_enabled() ) {
811 break;
812 }
813
814 $result = 'full_sync' === $type ? self::$sender->do_full_sync() : self::$sender->do_sync();
815
816 // # of send actions performed.
817 ++$executions;
818
819 } while ( $result && ! is_wp_error( $result ) && ( $start_time + $time_limit ) > time() );
820
821 return $executions;
822 }
823
824 /**
825 * Initialize the sync listener.
826 *
827 * @access public
828 * @static
829 */
830 public static function initialize_listener() {
831 self::$listener = Listener::get_instance();
832 }
833
834 /**
835 * Initializes the sync sender.
836 *
837 * @access public
838 * @static
839 */
840 public static function initialize_sender() {
841 self::$sender = Sender::get_instance();
842 add_filter( 'jetpack_sync_send_data', array( __CLASS__, 'send_data' ), 10, 8 );
843 }
844
845 /**
846 * Initializes sync for WooCommerce.
847 *
848 * @access public
849 * @static
850 */
851 public static function initialize_woocommerce() {
852 if ( ! class_exists( 'WooCommerce' ) ) {
853 return;
854 }
855 add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_woocommerce_sync_module' ) );
856
857 if ( ! class_exists( CustomOrdersTableController::class ) ) {
858 return;
859 }
860 $cot_controller = wc_get_container()->get( CustomOrdersTableController::class );
861 if ( $cot_controller->custom_orders_table_usage_is_enabled() ) {
862 add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_woocommerce_hpos_order_sync_module' ) );
863 }
864 }
865
866 /**
867 * Initializes sync for Jetpack Search.
868 *
869 * The Search sync module owns the option-whitelist entries for
870 * `instant_search_enabled` and `jetpack_search_experience`. Registration
871 * is unconditional whenever the Search package is present — gating on
872 * either `is_instant_search_enabled()` or `is_active()` reintroduces a
873 * chicken-and-egg, because the very request that flips Search on (or
874 * flips Instant Search on) must already have the option whitelist in
875 * place to enqueue the write.
876 *
877 * The `class_exists()` guard below tracks package presence (autoloader
878 * concern), not module activation — `Module_Control` is autoloaded as
879 * long as the Search package is installed, even when the module is off.
880 *
881 * @access public
882 * @static
883 */
884 public static function initialize_search() {
885 if ( ! class_exists( 'Automattic\\Jetpack\\Search\\Module_Control' ) ) {
886 return;
887 }
888 add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_search_sync_module' ) );
889 }
890
891 /**
892 * Add Search updates to Sync Filters.
893 *
894 * @access public
895 * @static
896 *
897 * @param array $sync_modules The list of sync modules declared prior to this filter.
898 * @return array A list of sync modules that now includes Search's modules.
899 */
900 public static function add_search_sync_module( $sync_modules ) {
901 $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\Search';
902 return $sync_modules;
903 }
904
905 /**
906 * Adds Woo's sync modules to existing modules for sending.
907 *
908 * @access public
909 * @static
910 *
911 * @param array $sync_modules The list of sync modules declared prior to this filter.
912 * @return array A list of sync modules that now includes Woo's modules.
913 */
914 public static function add_woocommerce_sync_module( $sync_modules ) {
915 $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WooCommerce';
916 return $sync_modules;
917 }
918
919 /**
920 * Adds Woo's HPOS sync modules to existing modules for sending.
921 *
922 * @param array $sync_modules The list of sync modules declared prior to this filter.
923 *
924 * @access public
925 * @static
926 *
927 * @return array A list of sync modules that now includes Woo's HPOS modules.
928 */
929 public static function add_woocommerce_hpos_order_sync_module( $sync_modules ) {
930 $sync_modules[] = WooCommerce_HPOS_Orders::class;
931 return $sync_modules;
932 }
933
934 /**
935 * Adds Woo's Products sync module to existing modules for sending.
936 *
937 * Note: This module is currently used for WooCommerce Analytics only.
938 *
939 * @param array $sync_modules The list of sync modules declared prior to this filter.
940 *
941 * @access public
942 * @static
943 *
944 * @return array A list of sync modules that now includes Woo's Products module.
945 */
946 public static function add_woocommerce_products_sync_module( $sync_modules ) {
947 $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WooCommerce_Products';
948 return $sync_modules;
949 }
950
951 /**
952 * Initializes sync for WP Super Cache.
953 *
954 * @access public
955 * @static
956 */
957 public static function initialize_wp_super_cache() {
958 if ( ! function_exists( 'wp_cache_is_enabled' ) ) {
959 return;
960 }
961 add_filter( 'jetpack_sync_modules', array( __CLASS__, 'add_wp_super_cache_sync_module' ) );
962 }
963
964 /**
965 * Adds WP Super Cache's sync modules to existing modules for sending.
966 *
967 * @access public
968 * @static
969 *
970 * @param array $sync_modules The list of sync modules declared prior to this filer.
971 * @return array A list of sync modules that now includes WP Super Cache's modules.
972 */
973 public static function add_wp_super_cache_sync_module( $sync_modules ) {
974 $sync_modules[] = 'Automattic\\Jetpack\\Sync\\Modules\\WP_Super_Cache';
975 return $sync_modules;
976 }
977
978 /**
979 * Sanitizes the name of sync's cron schedule.
980 *
981 * @access public
982 * @static
983 *
984 * @param string $schedule The name of a WordPress cron schedule.
985 * @return string The sanitized name of sync's cron schedule.
986 */
987 public static function sanitize_filtered_sync_cron_schedule( $schedule ) {
988 $schedule = sanitize_key( $schedule );
989 $schedules = wp_get_schedules();
990
991 // Make sure that the schedule has actually been registered using the `cron_intervals` filter.
992 if ( isset( $schedules[ $schedule ] ) ) {
993 return $schedule;
994 }
995
996 return self::DEFAULT_SYNC_CRON_INTERVAL_NAME;
997 }
998
999 /**
1000 * Allows offsetting of start times for sync cron jobs.
1001 *
1002 * @access public
1003 * @static
1004 *
1005 * @param string $schedule The name of a cron schedule.
1006 * @param string $hook The hook that this method is responding to.
1007 * @return int The offset for the sync cron schedule.
1008 */
1009 public static function get_start_time_offset( $schedule = '', $hook = '' ) {
1010 $start_time_offset = is_multisite()
1011 ? wp_rand( 0, ( 2 * self::DEFAULT_SYNC_CRON_INTERVAL_VALUE ) )
1012 : 0;
1013
1014 /**
1015 * Allows overriding the offset that the sync cron jobs will first run. This can be useful when scheduling
1016 * cron jobs across multiple sites in a network.
1017 *
1018 * @since 1.6.3
1019 * @since-jetpack 4.5.0
1020 *
1021 * @param int $start_time_offset
1022 * @param string $hook
1023 * @param string $schedule
1024 */
1025 return (int) apply_filters(
1026 'jetpack_sync_cron_start_time_offset',
1027 $start_time_offset,
1028 $hook,
1029 $schedule
1030 );
1031 }
1032
1033 /**
1034 * Decides if a sync cron should be scheduled.
1035 *
1036 * @access public
1037 * @static
1038 *
1039 * @param string $schedule The name of a cron schedule.
1040 * @param string $hook The hook that this method is responding to.
1041 */
1042 public static function maybe_schedule_sync_cron( $schedule, $hook ) {
1043 if ( ! $hook ) {
1044 return;
1045 }
1046 $schedule = self::sanitize_filtered_sync_cron_schedule( $schedule );
1047
1048 $start_time = time() + self::get_start_time_offset( $schedule, $hook );
1049 if ( ! wp_next_scheduled( $hook ) ) {
1050 // Schedule a job to send pending queue items once a minute.
1051 wp_schedule_event( $start_time, $schedule, $hook );
1052 } elseif ( wp_get_schedule( $hook ) !== $schedule ) {
1053 // If the schedule has changed, update the schedule.
1054 wp_clear_scheduled_hook( $hook );
1055 wp_schedule_event( $start_time, $schedule, $hook );
1056 }
1057 }
1058
1059 /**
1060 * Clears Jetpack sync cron jobs.
1061 *
1062 * @access public
1063 * @static
1064 */
1065 public static function clear_sync_cron_jobs() {
1066 wp_clear_scheduled_hook( 'jetpack_sync_cron' );
1067 wp_clear_scheduled_hook( 'jetpack_sync_full_cron' );
1068 }
1069
1070 /**
1071 * Initializes Jetpack sync cron jobs.
1072 *
1073 * @access public
1074 * @static
1075 */
1076 public static function init_sync_cron_jobs() {
1077 add_filter( 'cron_schedules', array( __CLASS__, 'jetpack_cron_schedule' ) ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected
1078
1079 add_action( 'jetpack_sync_cron', array( __CLASS__, 'do_cron_sync' ) );
1080 add_action( 'jetpack_sync_full_cron', array( __CLASS__, 'do_cron_full_sync' ) );
1081
1082 /**
1083 * Allows overriding of the default incremental sync cron schedule which defaults to once every 5 minutes.
1084 *
1085 * @since 1.6.3
1086 * @since-jetpack 4.3.2
1087 *
1088 * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME
1089 */
1090 $incremental_sync_cron_schedule = apply_filters( 'jetpack_sync_incremental_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME );
1091 self::maybe_schedule_sync_cron( $incremental_sync_cron_schedule, 'jetpack_sync_cron' );
1092
1093 /**
1094 * Allows overriding of the full sync cron schedule which defaults to once every 5 minutes.
1095 *
1096 * @since 1.6.3
1097 * @since-jetpack 4.3.2
1098 *
1099 * @param string self::DEFAULT_SYNC_CRON_INTERVAL_NAME
1100 */
1101 $full_sync_cron_schedule = apply_filters( 'jetpack_sync_full_sync_interval', self::DEFAULT_SYNC_CRON_INTERVAL_NAME );
1102 self::maybe_schedule_sync_cron( $full_sync_cron_schedule, 'jetpack_sync_full_cron' );
1103 }
1104
1105 /**
1106 * Perform maintenance when a plugin upgrade occurs.
1107 *
1108 * @access public
1109 * @static
1110 *
1111 * @param string $new_version New version of the plugin.
1112 * @param string $old_version Old version of the plugin.
1113 */
1114 public static function cleanup_on_upgrade( $new_version = '', $old_version = '' ) {
1115 if ( wp_next_scheduled( 'jetpack_sync_send_db_checksum' ) ) {
1116 wp_clear_scheduled_hook( 'jetpack_sync_send_db_checksum' );
1117 }
1118
1119 $is_new_sync_upgrade = version_compare( $old_version, '4.2', '>=' );
1120 if ( ! empty( $old_version ) && $is_new_sync_upgrade && version_compare( $old_version, '4.5', '<' ) ) {
1121 self::clear_sync_cron_jobs();
1122 Settings::update_settings(
1123 array(
1124 'render_filtered_content' => Defaults::$default_render_filtered_content,
1125 )
1126 );
1127 }
1128
1129 Health::on_jetpack_upgraded();
1130 }
1131
1132 /**
1133 * Get syncing status for the given fields.
1134 *
1135 * @access public
1136 * @static
1137 *
1138 * @param string|null $fields A comma-separated string of the fields to include in the array from the JSON response.
1139 * @return array An associative array with the status report.
1140 */
1141 public static function get_sync_status( $fields = null ) {
1142 self::initialize_sender();
1143
1144 $sync_module = Modules::get_module( 'full-sync' );
1145 '@phan-var Modules\Full_Sync_Immediately|Modules\Full_Sync $sync_module';
1146 $queue = self::$sender->get_sync_queue();
1147
1148 // _get_cron_array can be false
1149 $cron_timestamps = ( _get_cron_array() ) ? array_keys( _get_cron_array() ) : array();
1150 $next_cron = ( ! empty( $cron_timestamps ) ) ? $cron_timestamps[0] - time() : '';
1151
1152 $checksums = array();
1153 $debug = array();
1154
1155 if ( ! empty( $fields ) ) {
1156 $store = new Replicastore();
1157 $fields_params = array_map( 'trim', explode( ',', $fields ) );
1158
1159 if ( in_array( 'posts_checksum', $fields_params, true ) ) {
1160 $checksums['posts_checksum'] = $store->posts_checksum();
1161 }
1162 if ( in_array( 'comments_checksum', $fields_params, true ) ) {
1163 $checksums['comments_checksum'] = $store->comments_checksum();
1164 }
1165 if ( in_array( 'post_meta_checksum', $fields_params, true ) ) {
1166 $checksums['post_meta_checksum'] = $store->post_meta_checksum();
1167 }
1168 if ( in_array( 'comment_meta_checksum', $fields_params, true ) ) {
1169 $checksums['comment_meta_checksum'] = $store->comment_meta_checksum();
1170 }
1171
1172 if ( in_array( 'debug_details', $fields_params, true ) ) {
1173 $debug = self::get_debug_details();
1174 }
1175 }
1176
1177 $full_sync_status = ( $sync_module ) ? $sync_module->get_status() : array();
1178
1179 $full_queue = self::$sender->get_full_sync_queue();
1180
1181 $result = array_merge(
1182 $full_sync_status,
1183 $checksums,
1184 $debug,
1185 array(
1186 'cron_size' => count( $cron_timestamps ),
1187 'next_cron' => $next_cron,
1188 'queue_size' => $queue->size(),
1189 'queue_lag' => $queue->lag(),
1190 'queue_next_sync' => ( self::$sender->get_next_sync_time( 'sync' ) - microtime( true ) ),
1191 'full_queue_next_sync' => ( self::$sender->get_next_sync_time( 'full_sync' ) - microtime( true ) ),
1192 )
1193 );
1194
1195 // Verify $sync_module is not false.
1196 if ( $sync_module && ! $sync_module instanceof Modules\Full_Sync_Immediately ) {
1197 $result['full_queue_size'] = $full_queue->size();
1198 $result['full_queue_lag'] = $full_queue->lag();
1199 }
1200 return $result;
1201 }
1202
1203 /**
1204 * Reset Sync locks.
1205 *
1206 * @access public
1207 * @static
1208 * @since 1.43.0
1209 *
1210 * @param bool $unlock_queues Whether to unlock Sync queues. Defaults to true.
1211 */
1212 public static function reset_sync_locks( $unlock_queues = true ) {
1213 // Next sync locks.
1214 delete_option( Sender::NEXT_SYNC_TIME_OPTION_NAME . '_sync' );
1215 delete_option( Sender::NEXT_SYNC_TIME_OPTION_NAME . '_full_sync' );
1216 delete_option( Sender::NEXT_SYNC_TIME_OPTION_NAME . '_full-sync-enqueue' );
1217 // Retry after locks.
1218 delete_option( self::RETRY_AFTER_PREFIX . 'sync' );
1219 delete_option( self::RETRY_AFTER_PREFIX . 'full_sync' );
1220 // Dedicated sync locks.
1221 $dedicated_sync_lock_option = Dedicated_Sender::DEDICATED_SYNC_REQUEST_LOCK_OPTION_NAME;
1222 $dedicated_sync_lock_expires_option = $dedicated_sync_lock_option . '_expires';
1223 \Jetpack_Options::delete_raw_option( $dedicated_sync_lock_option );
1224 \Jetpack_Options::delete_raw_option( $dedicated_sync_lock_expires_option );
1225
1226 delete_transient( Dedicated_Sender::DEDICATED_SYNC_TEMPORARY_DISABLE_FLAG );
1227 // Lock for disabling Sync sending temporarily.
1228 delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
1229
1230 // Queue locks.
1231 // Note that we are just unlocking the queues here, not reseting them.
1232 if ( $unlock_queues ) {
1233 $sync_queue = new Queue( 'sync' );
1234 $sync_queue->unlock();
1235
1236 $full_sync_queue = new Queue( 'full_sync' );
1237 $full_sync_queue->unlock();
1238 }
1239 }
1240
1241 /**
1242 * Prepare JSONL data.
1243 *
1244 * @param mixed $data The data to be prepared.
1245 *
1246 * @return string The prepared JSONL data.
1247 */
1248 private static function prepare_jsonl_data( $data ) {
1249 $jsonl_data = implode(
1250 "\n",
1251 array_map(
1252 function ( $key, $value ) {
1253 return wp_json_encode( array( $key => $value ), JSON_UNESCAPED_SLASHES );
1254 },
1255 array_keys( (array) $data ),
1256 array_values( (array) $data )
1257 )
1258 );
1259 return $jsonl_data;
1260 }
1261
1262 /**
1263 * Helper method to process the API response.
1264 *
1265 * @param mixed $response The response from the API.
1266 * @return array|Wp_Error Array for successful response or a WP_Error object.
1267 */
1268 private static function process_rest_api_response( $response ) {
1269
1270 $response_code = wp_remote_retrieve_response_code( $response );
1271 $response_body = wp_remote_retrieve_body( $response );
1272 if ( is_wp_error( $response ) ) {
1273 return $response;
1274 }
1275 $decoded_response = json_decode( $response_body, true );
1276
1277 if ( ! is_array( $decoded_response ) ) {
1278 return new WP_Error( 'sync_rest_api_response_decoding_failed', 'Sync REST API response decoding failed', $response_body );
1279 }
1280
1281 if ( $response_code !== 200 || ! isset( $decoded_response['processed_items'] ) ) {
1282 if ( isset( $decoded_response['code'] ) && isset( $decoded_response['message'] ) ) {
1283 return new WP_Error(
1284 'jetpack_sync_send_error_' . $decoded_response['code'],
1285 $decoded_response['message'],
1286 $decoded_response['data'] ?? null
1287 );
1288 } else {
1289 return new WP_Error( $response_code, 'Sync REST API request failed', $response_body );
1290 }
1291 } else {
1292 return $decoded_response;
1293 }
1294 }
1295 }
1296