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-actions.php +1295 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1295 @@
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 +}