Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.75% covered (warning)
75.75%
428 / 565
28.57% covered (danger)
28.57%
6 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Endpoints
75.75% covered (warning)
75.75%
428 / 565
28.57% covered (danger)
28.57%
6 / 21
152.07
0.00% covered (danger)
0.00%
0 / 1
 initialize_rest_api
100.00% covered (success)
100.00%
321 / 321
100.00% covered (success)
100.00%
1 / 1
1
 full_sync_start
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 sync_status
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 data_check
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 data_histogram
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 sync_health
75.00% covered (warning)
75.00%
27 / 36
0.00% covered (danger)
0.00%
0 / 1
5.39
 get_sync_settings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_sync_settings
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 get_sync_objects
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 do_sync
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 checkout
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
12.49
 unlock_queue
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 close
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
110
 get_object_id_range
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 spawn_sync
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
2.29
 reset_locks
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 clear_queue
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 verify_default_permissions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 validate_queue
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 is_valid_sync_module
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 sanitize_item_ids
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Sync package.
4 *
5 * @package  automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync;
9
10use Automattic\Jetpack\Connection\Rest_Authentication;
11use WP_Error;
12use WP_REST_Server;
13
14/**
15 * This class will handle Sync v4 REST Endpoints.
16 *
17 * @since 1.23.1
18 */
19class REST_Endpoints {
20
21    /**
22     *  Items pending send.
23     *
24     * @var array
25     */
26    public $items = array();
27
28    /**
29     * Initialize REST routes.
30     */
31    public static function initialize_rest_api() {
32
33        // Request a Full Sync.
34        register_rest_route(
35            'jetpack/v4',
36            '/sync/full-sync',
37            array(
38                'methods'             => WP_REST_Server::EDITABLE,
39                'callback'            => __CLASS__ . '::full_sync_start',
40                'permission_callback' => __CLASS__ . '::verify_default_permissions',
41                'args'                => array(
42                    'modules'  => array(
43                        'description' => __( 'Data Modules that should be included in Full Sync', 'jetpack-sync' ),
44                        'type'        => 'array',
45                        'required'    => false,
46                    ),
47                    'users'    => array(
48                        'description' => __( 'User IDs to include in Full Sync or "initial"', 'jetpack-sync' ),
49                        'required'    => false,
50                    ),
51                    'posts'    => array(
52                        'description' => __( 'Post IDs to include in Full Sync', 'jetpack-sync' ),
53                        'type'        => 'array',
54                        'required'    => false,
55                    ),
56                    'comments' => array(
57                        'description' => __( 'Comment IDs to include in Full Sync', 'jetpack-sync' ),
58                        'type'        => 'array',
59                        'required'    => false,
60                    ),
61                    'context'  => array(
62                        'description' => __( 'Context for the Full Sync', 'jetpack-sync' ),
63                        'type'        => 'string',
64                        'required'    => false,
65                    ),
66                ),
67            )
68        );
69
70        // Obtain Sync status.
71        register_rest_route(
72            'jetpack/v4',
73            '/sync/status',
74            array(
75                'methods'             => WP_REST_Server::READABLE,
76                'callback'            => __CLASS__ . '::sync_status',
77                'permission_callback' => __CLASS__ . '::verify_default_permissions',
78                'args'                => array(
79                    'fields' => array(
80                        'description' => __( 'Comma-separated list of additional fields that should be included in status.', 'jetpack-sync' ),
81                        'type'        => 'string',
82                        'required'    => false,
83                    ),
84                ),
85            )
86        );
87
88        // Update Sync health status.
89        register_rest_route(
90            'jetpack/v4',
91            '/sync/health',
92            array(
93                'methods'             => WP_REST_Server::EDITABLE,
94                'callback'            => __CLASS__ . '::sync_health',
95                'permission_callback' => __CLASS__ . '::verify_default_permissions',
96                'args'                => array(
97                    'status' => array(
98                        'description' => __( 'New Sync health status', 'jetpack-sync' ),
99                        'type'        => 'string',
100                        'required'    => true,
101                    ),
102                ),
103            )
104        );
105
106        // Obtain Sync settings.
107        register_rest_route(
108            'jetpack/v4',
109            '/sync/settings',
110            array(
111                array(
112                    'methods'             => WP_REST_Server::READABLE,
113                    'callback'            => __CLASS__ . '::get_sync_settings',
114                    'permission_callback' => __CLASS__ . '::verify_default_permissions',
115                ),
116                array(
117                    'methods'             => WP_REST_Server::EDITABLE,
118                    'callback'            => __CLASS__ . '::update_sync_settings',
119                    'permission_callback' => __CLASS__ . '::verify_default_permissions',
120                ),
121            )
122        );
123
124        // Retrieve Sync Object(s).
125        register_rest_route(
126            'jetpack/v4',
127            '/sync/object',
128            array(
129                'methods'             => WP_REST_Server::ALLMETHODS,
130                'callback'            => __CLASS__ . '::get_sync_objects',
131                'permission_callback' => __CLASS__ . '::verify_default_permissions',
132                'args'                => array(
133                    'module_name' => array(
134                        'description' => __( 'Name of Sync module', 'jetpack-sync' ),
135                        'type'        => 'string',
136                        'required'    => false,
137                    ),
138                    'object_type' => array(
139                        'description' => __( 'Object Type', 'jetpack-sync' ),
140                        'type'        => 'string',
141                        'required'    => false,
142                    ),
143                    'object_ids'  => array(
144                        'description' => __( 'Objects Identifiers', 'jetpack-sync' ),
145                        'type'        => 'array',
146                        'required'    => false,
147                    ),
148                ),
149            )
150        );
151
152        // Retrieve Sync Object(s).
153        register_rest_route(
154            'jetpack/v4',
155            '/sync/now',
156            array(
157                'methods'             => WP_REST_Server::EDITABLE,
158                'callback'            => __CLASS__ . '::do_sync',
159                'permission_callback' => __CLASS__ . '::verify_default_permissions',
160                'args'                => array(
161                    'queue' => array(
162                        'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
163                        'type'        => 'string',
164                        'required'    => true,
165                    ),
166                ),
167            )
168        );
169
170        // Checkout Sync Objects.
171        register_rest_route(
172            'jetpack/v4',
173            '/sync/checkout',
174            array(
175                'methods'             => WP_REST_Server::EDITABLE,
176                'callback'            => __CLASS__ . '::checkout',
177                'permission_callback' => __CLASS__ . '::verify_default_permissions',
178                'args'                => array(
179                    'queue'            => array(
180                        'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
181                        'type'        => 'string',
182                        'required'    => false,
183                    ),
184                    'number_of_items'  => array(
185                        'description' => __( 'Number of items to checkout from the queue.', 'jetpack-sync' ),
186                        'type'        => 'integer',
187                        'required'    => false,
188                    ),
189                    'pop'              => array(
190                        'description'       => __( 'Pop items from the queue instead of checking out.', 'jetpack-sync' ),
191                        'type'              => 'boolean',
192                        'required'          => false,
193                        'sanitize_callback' => 'rest_sanitize_boolean',
194                    ),
195                    'force'            => array(
196                        'description'       => __( 'Force unlock the queue before checkout.', 'jetpack-sync' ),
197                        'type'              => 'boolean',
198                        'required'          => false,
199                        'sanitize_callback' => 'rest_sanitize_boolean',
200                    ),
201                    'encode'           => array(
202                        'description'       => __( 'Encode the items before sending.', 'jetpack-sync' ),
203                        'type'              => 'boolean',
204                        'required'          => false,
205                        'default'           => true,
206                        'sanitize_callback' => 'rest_sanitize_boolean',
207                    ),
208                    'use_memory_limit' => array(
209                        'description'       => __( 'Use memory-based checkout instead of fixed item count.', 'jetpack-sync' ),
210                        'type'              => 'boolean',
211                        'required'          => false,
212                        'sanitize_callback' => 'rest_sanitize_boolean',
213                    ),
214                ),
215            )
216        );
217
218        // Checkin Sync Objects.
219        register_rest_route(
220            'jetpack/v4',
221            '/sync/close',
222            array(
223                'methods'             => WP_REST_Server::EDITABLE,
224                'callback'            => __CLASS__ . '::close',
225                'permission_callback' => __CLASS__ . '::verify_default_permissions',
226            )
227        );
228
229        // Unlock Sync Queue.
230        register_rest_route(
231            'jetpack/v4',
232            '/sync/unlock',
233            array(
234                'methods'             => WP_REST_Server::EDITABLE,
235                'callback'            => __CLASS__ . '::unlock_queue',
236                'permission_callback' => __CLASS__ . '::verify_default_permissions',
237                'args'                => array(
238                    'queue' => array(
239                        'description' => __( 'Name of Sync queue.', 'jetpack-sync' ),
240                        'type'        => 'string',
241                        'required'    => true,
242                    ),
243                ),
244            )
245        );
246
247        // Retrieve range of Object Ids.
248        register_rest_route(
249            'jetpack/v4',
250            '/sync/object-id-range',
251            array(
252                'methods'             => WP_REST_Server::READABLE,
253                'callback'            => __CLASS__ . '::get_object_id_range',
254                'permission_callback' => __CLASS__ . '::verify_default_permissions',
255                'args'                => array(
256                    'sync_module' => array(
257                        'description' => __( 'Name of Sync module.', 'jetpack-sync' ),
258                        'type'        => 'string',
259                        'required'    => true,
260                    ),
261                    'batch_size'  => array(
262                        'description' => __( 'Size of batches', 'jetpack-sync' ),
263                        'type'        => 'int',
264                        'required'    => true,
265                    ),
266                ),
267            )
268        );
269
270        // Obtain table checksums.
271        register_rest_route(
272            'jetpack/v4',
273            '/sync/data-check',
274            array(
275                'methods'             => WP_REST_Server::READABLE,
276                'callback'            => __CLASS__ . '::data_check',
277                'permission_callback' => __CLASS__ . '::verify_default_permissions',
278                'args'                => array(
279                    'perform_text_conversion' => array(
280                        'description' => __( 'If text fields should be converted to latin1 in checksum calculation.', 'jetpack-sync' ),
281                        'type'        => 'boolean',
282                        'required'    => false,
283                    ),
284                ),
285            )
286        );
287
288        // Obtain histogram.
289        register_rest_route(
290            'jetpack/v4',
291            '/sync/data-histogram',
292            array(
293                'methods'             => WP_REST_Server::EDITABLE,
294                'callback'            => __CLASS__ . '::data_histogram',
295                'permission_callback' => __CLASS__ . '::verify_default_permissions',
296                'args'                => array(
297                    'columns'                 => array(
298                        'description' => __( 'Column mappings', 'jetpack-sync' ),
299                        'type'        => 'array',
300                        'required'    => false,
301                    ),
302                    'object_type'             => array(
303                        'description' => __( 'Object Type', 'jetpack-sync' ),
304                        'type'        => 'string',
305                        'required'    => false,
306                    ),
307                    'buckets'                 => array(
308                        'description' => __( 'Number of histogram buckets.', 'jetpack-sync' ),
309                        'type'        => 'int',
310                        'required'    => false,
311                    ),
312                    'start_id'                => array(
313                        'description' => __( 'Start ID for the histogram', 'jetpack-sync' ),
314                        'type'        => 'int',
315                        'required'    => false,
316                    ),
317                    'end_id'                  => array(
318                        'description' => __( 'End ID for the histogram', 'jetpack-sync' ),
319                        'type'        => 'int',
320                        'required'    => false,
321                    ),
322                    'strip_non_ascii'         => array(
323                        'description' => __( 'Strip non-ascii characters?', 'jetpack-sync' ),
324                        'type'        => 'boolean',
325                        'required'    => false,
326                    ),
327                    'shared_salt'             => array(
328                        'description' => __( 'Shared Salt to use when generating checksum', 'jetpack-sync' ),
329                        'type'        => 'string',
330                        'required'    => false,
331                    ),
332                    'only_range_edges'        => array(
333                        'description' => __( 'Should only range edges be returned', 'jetpack-sync' ),
334                        'type'        => 'boolean',
335                        'required'    => false,
336                    ),
337                    'detailed_drilldown'      => array(
338                        'description' => __( 'Do we want the checksum or object ids.', 'jetpack-sync' ),
339                        'type'        => 'boolean',
340                        'required'    => false,
341                    ),
342                    'perform_text_conversion' => array(
343                        'description' => __( 'If text fields should be converted to latin1 in checksum calculation.', 'jetpack-sync' ),
344                        'type'        => 'boolean',
345                        'required'    => false,
346                    ),
347                ),
348            )
349        );
350
351        // Trigger Dedicated Sync request.
352        register_rest_route(
353            'jetpack/v4',
354            '/sync/spawn-sync',
355            array(
356                'methods'             => WP_REST_Server::READABLE,
357                'callback'            => __CLASS__ . '::spawn_sync',
358                'permission_callback' => '__return_true',
359            )
360        );
361
362        // Reset Sync locks.
363        register_rest_route(
364            'jetpack/v4',
365            '/sync/locks',
366            array(
367                'methods'             => WP_REST_Server::DELETABLE,
368                'callback'            => __CLASS__ . '::reset_locks',
369                'permission_callback' => __CLASS__ . '::verify_default_permissions',
370            )
371        );
372
373        // Clear Sync queue.
374        register_rest_route(
375            'jetpack/v4',
376            '/sync/clear-queue',
377            array(
378                'methods'             => WP_REST_Server::EDITABLE,
379                'callback'            => __CLASS__ . '::clear_queue',
380                'permission_callback' => __CLASS__ . '::verify_default_permissions',
381            )
382        );
383    }
384
385    /**
386     * Trigger a Full Sync of specified modules.
387     *
388     * @since 1.23.1
389     *
390     * @param \WP_REST_Request $request The request sent to the WP REST API.
391     *
392     * @return \WP_REST_Response|WP_Error
393     */
394    public static function full_sync_start( $request ) {
395
396        $modules = $request->get_param( 'modules' );
397
398        // convert list of modules into array format of "$modulename => true".
399        if ( ! empty( $modules ) ) {
400            $modules = array_map( '__return_true', array_flip( $modules ) );
401        }
402
403        // Process additional options.
404        foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
405            if ( 'users' === $module_name && 'initial' === $request->get_param( 'users' ) ) {
406                $modules['users'] = 'initial';
407            } elseif ( is_array( $request->get_param( $module_name ) ) ) {
408                $ids = $request->get_param( $module_name );
409                if ( array() !== $ids ) {
410                    $modules[ $module_name ] = $ids;
411                }
412            }
413        }
414
415        if ( empty( $modules ) ) {
416            $modules = null;
417        }
418
419        $context = $request->get_param( 'context' );
420
421        return rest_ensure_response(
422            array(
423                'scheduled' => Actions::do_full_sync( $modules, $context ),
424            )
425        );
426    }
427
428    /**
429     * Return Sync's status.
430     *
431     * @since 1.23.1
432     *
433     * @param \WP_REST_Request $request The request sent to the WP REST API.
434     *
435     * @return \WP_REST_Response
436     */
437    public static function sync_status( $request ) {
438        $fields = $request->get_param( 'fields' );
439        return rest_ensure_response( Actions::get_sync_status( $fields ) );
440    }
441
442    /**
443     * Return table checksums.
444     *
445     * @since 1.23.1
446     *
447     * @param \WP_REST_Request $request The request sent to the WP REST API.
448     *
449     * @return \WP_REST_Response
450     */
451    public static function data_check( $request ) {
452        // Disable Sync during this call, so we can resolve faster.
453        Actions::mark_sync_read_only();
454        $store = new Replicastore();
455
456        $perform_text_conversion = false;
457        if ( true === $request->get_param( 'perform_text_conversion' ) ) {
458            $perform_text_conversion = true;
459        }
460
461        return rest_ensure_response( $store->checksum_all( $perform_text_conversion ) );
462    }
463
464    /**
465     * Return Histogram.
466     *
467     * @since 1.23.1
468     *
469     * @param \WP_REST_Request $request The request sent to the WP REST API.
470     *
471     * @return \WP_REST_Response
472     */
473    public static function data_histogram( $request ) {
474
475        // Disable Sync during this call, so we can resolve faster.
476        Actions::mark_sync_read_only();
477
478        $args = $request->get_params();
479
480        if ( empty( $args['columns'] ) ) {
481            $args['columns'] = null; // go with defaults.
482        }
483
484        if ( false !== $args['strip_non_ascii'] ) {
485            $args['strip_non_ascii'] = true;
486        }
487
488        if ( true !== $args['perform_text_conversion'] ) {
489            $args['perform_text_conversion'] = false;
490        }
491
492        /**
493         * Hack: nullify the values of `start_id` and `end_id` if we're only requesting ranges.
494         *
495         * The endpoint doesn't support nullable values :(
496         */
497        if ( true === $args['only_range_edges'] ) {
498            if ( 0 === $args['start_id'] ) {
499                $args['start_id'] = null;
500            }
501
502            if ( 0 === $args['end_id'] ) {
503                $args['end_id'] = null;
504            }
505        }
506
507        $store     = new Replicastore();
508        $histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $args['columns'], $args['strip_non_ascii'], $args['shared_salt'], $args['only_range_edges'], $args['detailed_drilldown'], $args['perform_text_conversion'] );
509
510        return rest_ensure_response(
511            array(
512                'histogram' => $histogram,
513                'type'      => $store->get_checksum_type(),
514            )
515        );
516    }
517
518    /**
519     * Update Sync health.
520     *
521     * IN_SYNC is only set if the incremental queue is within size and lag limits.
522     *
523     * @since 1.23.1
524     *
525     * @param \WP_REST_Request $request The request sent to the WP REST API.
526     *
527     * @return \WP_REST_Response|WP_Error
528     */
529    public static function sync_health( $request ) {
530        $requested_status = $request->get_param( 'status' );
531
532        switch ( $requested_status ) {
533            case Health::STATUS_IN_SYNC:
534                // Only allow setting IN_SYNC if the incremental queue is healthy.
535                $sync_queue    = Listener::get_instance()->get_sync_queue();
536                $queue_size    = $sync_queue->size();
537                $queue_lag     = $sync_queue->lag();
538                $queue_healthy = Health::is_queue_healthy(
539                    $queue_size,
540                    $queue_lag,
541                    Settings::get_setting( 'max_queue_size' ),
542                    Settings::get_setting( 'max_queue_lag' )
543                );
544                if ( ! $queue_healthy ) {
545                    Health::update_status( Health::STATUS_OUT_OF_SYNC );
546                    return rest_ensure_response(
547                        array(
548                            'success'    => Health::get_status(),
549                            'message'    => 'Sync queue is not healthy (size and lag over limit). Status not set to in_sync.',
550                            'queue_size' => $queue_size,
551                            'queue_lag'  => $queue_lag,
552                        )
553                    );
554                }
555                Health::update_status( $requested_status );
556                break;
557            case Health::STATUS_OUT_OF_SYNC:
558                Health::update_status( $requested_status );
559                break;
560            default:
561                return new WP_Error(
562                    'invalid_status',
563                    'Invalid Sync Status Provided.',
564                    array(
565                        'status' => 400,
566                    )
567                );
568        }
569
570        // re-fetch so we see what's really being stored.
571        return rest_ensure_response(
572            array(
573                'success' => Health::get_status(),
574            )
575        );
576    }
577
578    /**
579     * Obtain Sync settings.
580     *
581     * @since 1.23.1
582     *
583     * @return \WP_REST_Response
584     */
585    public static function get_sync_settings() {
586        return rest_ensure_response( Settings::get_settings() );
587    }
588
589    /**
590     * Update Sync settings.
591     *
592     * @since 1.23.1
593     *
594     * @param \WP_REST_Request $request The request sent to the WP REST API.
595     *
596     * @return \WP_REST_Response
597     */
598    public static function update_sync_settings( $request ) {
599        $args          = $request->get_params();
600        $sync_settings = Settings::get_settings();
601
602        foreach ( $args as $key => $value ) {
603            if ( false !== $value ) {
604                if ( is_numeric( $value ) ) {
605                    $value = (int) $value;
606                }
607
608                // special case for sending empty arrays - a string with value 'empty'.
609                if ( 'empty' === $value ) {
610                    $value = array();
611                }
612
613                $sync_settings[ $key ] = $value;
614            }
615        }
616
617        Settings::update_settings( $sync_settings );
618
619        // re-fetch so we see what's really being stored.
620        return rest_ensure_response( Settings::get_settings() );
621    }
622
623    /**
624     * Retrieve Sync Objects.
625     *
626     * @since 1.23.1
627     *
628     * @param \WP_REST_Request $request The request sent to the WP REST API.
629     *
630     * @return \WP_REST_Response
631     */
632    public static function get_sync_objects( $request ) {
633        $args = $request->get_params();
634
635        $module_name = $args['module_name'];
636        // Verify valid Sync Module.
637        $sync_module = Modules::get_module( $module_name );
638        if ( ! $sync_module ) {
639            return new WP_Error( 'invalid_module', 'You specified an invalid sync module' );
640        }
641
642        Actions::mark_sync_read_only();
643
644        $codec = Sender::get_instance()->get_codec();
645        Settings::set_is_syncing( true );
646        $objects = $codec->encode( $sync_module->get_objects_by_id( $args['object_type'], $args['object_ids'] ) );
647        Settings::set_is_syncing( false );
648
649        return rest_ensure_response(
650            array(
651                'objects' => $objects,
652                'codec'   => $codec->name(),
653            )
654        );
655    }
656
657    /**
658     * Request Sync processing.
659     *
660     * @since 1.23.1
661     *
662     * @param \WP_REST_Request $request The request sent to the WP REST API.
663     *
664     * @return \WP_REST_Response
665     */
666    public static function do_sync( $request ) {
667
668        $queue_name = self::validate_queue( $request->get_param( 'queue' ) );
669        if ( is_wp_error( $queue_name ) ) {
670            return $queue_name;
671        }
672
673        $sender   = Sender::get_instance();
674        $response = $sender->do_sync_for_queue( new Queue( $queue_name ) );
675
676        return rest_ensure_response(
677            array(
678                'response' => $response,
679            )
680        );
681    }
682
683    /**
684     * Request sync data from specified queue.
685     *
686     * @since 1.23.1
687     *
688     * @param \WP_REST_Request $request The request sent to the WP REST API.
689     *
690     * @return \WP_REST_Response
691     */
692    public static function checkout( $request ) {
693        $args       = $request->get_params();
694        $queue_name = self::validate_queue( $args['queue'] );
695
696        if ( is_wp_error( $queue_name ) ) {
697            return $queue_name;
698        }
699
700        $use_memory_limit = ! empty( $args['use_memory_limit'] );
701
702        if ( $use_memory_limit && ! empty( $args['pop'] ) ) {
703            return new WP_Error( 'invalid_args', 'pop cannot be used with use_memory_limit', 400 );
704        }
705
706        if ( ! $use_memory_limit ) {
707            if ( empty( $args['number_of_items'] ) || $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) {
708                return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and up to 100', 400 );
709            }
710        }
711
712        // REST Sender.
713        $sender = new REST_Sender();
714
715        if ( 'immediate' === $queue_name ) {
716            return rest_ensure_response( $sender->immediate_full_sync_pull() );
717        }
718
719        $number_of_items = $use_memory_limit ? null : $args['number_of_items'];
720        $response        = $sender->queue_pull( $queue_name, $number_of_items, $args );
721        // Disable sending while pulling.
722        if ( ! is_wp_error( $response ) ) {
723            set_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME, time(), Sender::TEMP_SYNC_DISABLE_TRANSIENT_EXPIRY );
724        } elseif ( 'queue_size' === $response->get_error_code() ) {
725            // Re-enable sending if the queue is empty.
726            delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
727        }
728
729        return rest_ensure_response( $response );
730    }
731
732    /**
733     * Unlock a Sync queue.
734     *
735     * @since 1.23.1
736     *
737     * @param \WP_REST_Request $request The request sent to the WP REST API.
738     *
739     * @return \WP_REST_Response
740     */
741    public static function unlock_queue( $request ) {
742
743        $queue_name = $request->get_param( 'queue' );
744
745        if ( ! in_array( $queue_name, array( 'sync', 'full_sync' ), true ) ) {
746            return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
747        }
748        $queue = new Queue( $queue_name );
749
750        // False means that there was no lock to delete.
751        $response = $queue->unlock();
752        return rest_ensure_response(
753            array(
754                'success' => $response,
755            )
756        );
757    }
758
759    /**
760     * Checkin Sync actions.
761     *
762     * @since 1.23.1
763     *
764     * @param \WP_REST_Request $request The request sent to the WP REST API.
765     *
766     * @return \WP_REST_Response
767     */
768    public static function close( $request ) {
769
770        $request_body = $request->get_params();
771        $queue_name   = self::validate_queue( $request_body['queue'] );
772
773        if ( is_wp_error( $queue_name ) ) {
774            return $queue_name;
775        }
776
777        if ( empty( $request_body['buffer_id'] ) ) {
778            return new WP_Error( 'missing_buffer_id', 'Please provide a buffer id', 400 );
779        }
780
781        if ( ! is_array( $request_body['item_ids'] ) ) {
782            return new WP_Error( 'missing_item_ids', 'Please provide a list of item ids in the item_ids argument', 400 );
783        }
784
785        // Limit to A-Z,a-z,0-9,_,- .
786        $request_body['buffer_id'] = preg_replace( '/[^A-Za-z0-9\-_\.]/', '', $request_body['buffer_id'] );
787        $request_body['item_ids']  = array_filter( array_map( array( 'Automattic\Jetpack\Sync\REST_Endpoints', 'sanitize_item_ids' ), $request_body['item_ids'] ) );
788
789        $queue = new Queue( $queue_name );
790
791        $items = $queue->peek_by_id( $request_body['item_ids'] );
792
793        // Update Full Sync Status if queue is "full_sync".
794        if ( 'full_sync' === $queue_name ) {
795            $full_sync_module = Modules::get_module( 'full-sync' );
796            '@phan-var Modules\Full_Sync_Immediately|Modules\Full_Sync $full_sync_module';
797            $full_sync_module->update_sent_progress_action( $items );
798        }
799
800        $buffer   = new Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] );
801        $response = $queue->close( $buffer, $request_body['item_ids'] );
802
803        // Perform another checkout?
804        if ( isset( $request_body['continue'] ) && $request_body['continue'] ) {
805            if ( in_array( $queue_name, array( 'full_sync', 'immediate' ), true ) ) {
806                // Send Full Sync Actions.
807                Sender::get_instance()->do_full_sync();
808            } elseif ( $queue->has_any_items() ) {
809                // Send Incremental Sync Actions.
810                Sender::get_instance()->do_sync();
811            }
812        }
813
814        if ( is_wp_error( $response ) ) {
815            return $response;
816        }
817
818        return rest_ensure_response(
819            array(
820                'success' => $response,
821                'status'  => Actions::get_sync_status(),
822            )
823        );
824    }
825
826    /**
827     * Retrieve range of Object Ids for a specified Sync module.
828     *
829     * @since 1.23.1
830     *
831     * @param \WP_REST_Request $request The request sent to the WP REST API.
832     *
833     * @return \WP_REST_Response
834     */
835    public static function get_object_id_range( $request ) {
836
837        $module_name = $request->get_param( 'sync_module' );
838        $batch_size  = $request->get_param( 'batch_size' );
839
840        if ( ! self::is_valid_sync_module( $module_name ) ) {
841            return new WP_Error( 'invalid_module', 'This sync module cannot be used to calculate a range.', 400 );
842        }
843        $module = Modules::get_module( $module_name );
844
845        return rest_ensure_response(
846            array(
847                'ranges' => $module->get_min_max_object_ids_for_batches( $batch_size ),
848            )
849        );
850    }
851
852    /**
853     * This endpoint is used by Sync to spawn a
854     * dedicated Sync request which will trigger Sync to run.
855     *
856     * If Dedicated Sync is enabled, this callback should never run as
857     * processing of Sync actions will occur earlier and exit.
858     *
859     * @see Actions::init
860     * @see Sender::do_dedicated_sync_and_exit
861     *
862     * @since 1.34.0
863     *
864     * @return \WP_REST_Response
865     */
866    public static function spawn_sync() {
867        nocache_headers();
868
869        if ( ! Settings::is_dedicated_sync_enabled() ) {
870            return new WP_Error(
871                'dedicated_sync_disabled',
872                'Dedicated Sync flow is disabled.',
873                array( 'status' => 422 )
874            );
875        }
876
877        return new WP_Error(
878            'dedicated_sync_failed',
879            'Failed to process Dedicated Sync request',
880            array( 'status' => 500 )
881        );
882    }
883
884    /**
885     * Reset Sync locks.
886     *
887     * @since 1.43.0
888     *
889     * @return \WP_REST_Response
890     */
891    public static function reset_locks() {
892        Actions::reset_sync_locks();
893
894        return rest_ensure_response(
895            array(
896                'success' => true,
897            )
898        );
899    }
900
901    /**
902     * Clear the Sync queue.
903     *
904     * @since 4.30.0
905     *
906     * @return \WP_REST_Response
907     */
908    public static function clear_queue() {
909        $queue = new Queue( 'sync' );
910        $queue->reset();
911
912        // Re-enable sending in case it was temporarily disabled during a pull.
913        delete_transient( Sender::TEMP_SYNC_DISABLE_TRANSIENT_NAME );
914
915        return rest_ensure_response(
916            array(
917                'success' => true,
918            )
919        );
920    }
921
922    /**
923     * Verify that request has default permissions to perform sync actions.
924     *
925     * @since 1.23.1
926     *
927     * @return bool Whether user has capability 'manage_options' or a blog token is used.
928     */
929    public static function verify_default_permissions() {
930        if ( current_user_can( 'manage_options' ) || Rest_Authentication::is_signed_with_blog_token() ) {
931            return true;
932        }
933
934        $error_msg = esc_html__(
935            'You do not have the correct user permissions to perform this action.
936            Please contact your site admin if you think this is a mistake.',
937            'jetpack-sync'
938        );
939
940        return new WP_Error( 'invalid_user_permission_sync', $error_msg, array( 'status' => rest_authorization_required_code() ) );
941    }
942
943    /**
944     * Validate Queue name.
945     *
946     * @param string $value Queue Name.
947     *
948     * @return WP_Error
949     */
950    protected static function validate_queue( $value ) {
951        if ( ! isset( $value ) ) {
952            return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
953        }
954
955        if ( ! in_array( $value, array( 'sync', 'full_sync', 'immediate' ), true ) ) {
956            return new WP_Error( 'invalid_queue', 'Queue name should be sync, full_sync or immediate', 400 );
957        }
958        return $value;
959    }
960
961    /**
962     * Validate name is a valid Sync module.
963     *
964     * @param string $module_name Name of Sync Module.
965     *
966     * @return bool
967     */
968    protected static function is_valid_sync_module( $module_name ) {
969        return in_array(
970            $module_name,
971            array(
972                'comments',
973                'posts',
974                'terms',
975                'term_relationships',
976                'users',
977            ),
978            true
979        );
980    }
981
982    /**
983     * Sanitize Item Ids.
984     *
985     * @param string $item Sync item identifier.
986     *
987     * @return string|string[]|null
988     */
989    protected static function sanitize_item_ids( $item ) {
990        // lets not delete any options that don't start with jpsq_sync- .
991        if ( ! is_string( $item ) || ! str_starts_with( $item, 'jpsq_' ) ) {
992            return null;
993        }
994        // Limit to A-Z,a-z,0-9,_,-,. .
995        return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item );
996    }
997}