Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
42.17% covered (danger)
42.17%
183 / 434
15.38% covered (danger)
15.38%
6 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
Actions
42.17% covered (danger)
42.17%
183 / 434
15.38% covered (danger)
15.38%
6 / 39
5431.47
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 add_sender_shutdown
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 add_dedicated_sync_sender_init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 mark_sync_read_only
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 should_initialize_sender
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
 should_initialize_sender_enqueue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 sync_allowed
60.00% covered (warning)
60.00%
9 / 15
0.00% covered (danger)
0.00%
0 / 1
14.18
 get_debug_details
59.46% covered (warning)
59.46%
22 / 37
0.00% covered (danger)
0.00%
0 / 1
14.40
 sync_via_cron_allowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prevent_publicize_blacklisted_posts
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 set_is_importing_true
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 send_data
60.49% covered (warning)
60.49%
49 / 81
0.00% covered (danger)
0.00%
0 / 1
48.19
 do_initial_sync
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
5.09
 do_only_first_initial_sync
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 do_full_sync
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 jetpack_cron_schedule
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 do_cron_sync
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 do_cron_full_sync
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 do_cron_sync_by_type
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
240
 initialize_listener
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initialize_sender
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 initialize_woocommerce
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 initialize_search
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 add_search_sync_module
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 add_woocommerce_sync_module
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_hpos_order_sync_module
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_woocommerce_products_sync_module
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 initialize_wp_super_cache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 add_wp_super_cache_sync_module
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_filtered_sync_cron_schedule
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 get_start_time_offset
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 maybe_schedule_sync_cron
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 clear_sync_cron_jobs
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 init_sync_cron_jobs
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 cleanup_on_upgrade
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 get_sync_status
84.62% covered (warning)
84.62%
33 / 39
0.00% covered (danger)
0.00%
0 / 1
12.52
 reset_sync_locks
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 prepare_jsonl_data
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 process_rest_api_response
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
1<?php
2/**
3 * A class that defines syncable actions for Jetpack.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Connection\Manager as Jetpack_Connection;
12use Automattic\Jetpack\Constants;
13use Automattic\Jetpack\Identity_Crisis;
14use Automattic\Jetpack\Status;
15use Automattic\Jetpack\Sync\Modules\WooCommerce_HPOS_Orders;
16use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
17use 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 */
25class 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}