PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | json-endpoints/jetpack/class.jetpack-json-api-sync-endpoint.php +666 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,666 @@
1 +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +
3 +use Automattic\Jetpack\Sync\Actions;
4 +use Automattic\Jetpack\Sync\Health;
5 +use Automattic\Jetpack\Sync\Modules;
6 +use Automattic\Jetpack\Sync\Queue;
7 +use Automattic\Jetpack\Sync\Queue_Buffer;
8 +use Automattic\Jetpack\Sync\Replicastore;
9 +use Automattic\Jetpack\Sync\Sender;
10 +use Automattic\Jetpack\Sync\Settings;
11 +
12 +if ( ! defined( 'ABSPATH' ) ) {
13 + exit( 0 );
14 +}
15 +
16 +// phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound
17 +
18 +/**
19 + * Sync endpoint class.
20 + *
21 + * POST /sites/%s/sync
22 + *
23 + * @phan-constructor-used-for-side-effects
24 + */
25 +class Jetpack_JSON_API_Sync_Endpoint extends Jetpack_JSON_API_Endpoint {
26 +
27 + /**
28 + * This endpoint allows authentication both via a blog and a user token.
29 + * If a user token is used, that user should have `manage_options` capability.
30 + *
31 + * @var array|string
32 + */
33 + protected $needed_capabilities = 'manage_options';
34 +
35 + /**
36 + * Validate the call.
37 + *
38 + * @param int $_blog_id - the blog ID.
39 + * @param string $capability - the capability.
40 + * @param bool $check_manage_active - unused.
41 + *
42 + * @return bool|WP_Error
43 + */
44 + protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
45 + return parent::validate_call( $_blog_id, $capability, false );
46 + }
47 +
48 + /**
49 + * The result.
50 + *
51 + * @return array
52 + */
53 + protected function result() {
54 + $args = $this->input();
55 + $modules = null;
56 +
57 + // convert list of modules in comma-delimited format into an array
58 + // of "$modulename => true"
59 + if ( isset( $args['modules'] ) && ! empty( $args['modules'] ) ) {
60 + $modules = array_map( '__return_true', array_flip( array_map( 'trim', explode( ',', $args['modules'] ) ) ) );
61 + }
62 +
63 + foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
64 + if ( 'users' === $module_name && isset( $args[ $module_name ] ) && 'initial' === $args[ $module_name ] ) {
65 + $modules['users'] = 'initial';
66 + } elseif ( isset( $args[ $module_name ] ) ) {
67 + $ids = explode( ',', $args[ $module_name ] );
68 + if ( is_countable( $ids ) && count( $ids ) > 0 ) {
69 + $modules[ $module_name ] = $ids;
70 + }
71 + }
72 + }
73 +
74 + if ( empty( $modules ) ) {
75 + $modules = null;
76 + }
77 + return array( 'scheduled' => Actions::do_full_sync( $modules, 'jetpack_json_api_sync_endpoint' ) );
78 + }
79 +
80 + /**
81 + * Validate the queue.
82 + *
83 + * @param array $query - the query.
84 + *
85 + * @return string|WP_Error
86 + */
87 + protected function validate_queue( $query ) {
88 + if ( ! isset( $query ) ) {
89 + return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
90 + }
91 +
92 + if ( ! in_array( $query, array( 'sync', 'full_sync', 'immediate' ), true ) ) {
93 + return new WP_Error( 'invalid_queue', 'Queue name should be sync, full_sync or immediate', 400 );
94 + }
95 + return $query;
96 + }
97 +}
98 +
99 +/**
100 + * Sync status endpoint class.
101 + *
102 + * GET /sites/%s/sync/status
103 + *
104 + * @phan-constructor-used-for-side-effects
105 + */
106 +class Jetpack_JSON_API_Sync_Status_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
107 + /**
108 + * Callback for the endpoint.
109 + *
110 + * @return array
111 + */
112 + protected function result() {
113 + $args = $this->query_args();
114 + $fields = $args['fields'] ?? array();
115 + return Actions::get_sync_status( $fields );
116 + }
117 +}
118 +
119 +/**
120 + * Sync Check Endpoint class.
121 + * GET /sites/%s/data-check
122 + *
123 + * @phan-constructor-used-for-side-effects
124 + */
125 +class Jetpack_JSON_API_Sync_Check_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
126 + /**
127 + * Callback for the endpoint.
128 + *
129 + * @return array
130 + */
131 + protected function result() {
132 + Actions::mark_sync_read_only();
133 + $store = new Replicastore();
134 + return $store->checksum_all();
135 + }
136 +}
137 +
138 +/**
139 + * Sync histogram endpoint.
140 + * GET /sites/%s/data-histogram
141 + *
142 + * @phan-constructor-used-for-side-effects
143 + */
144 +class Jetpack_JSON_API_Sync_Histogram_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
145 + /**
146 + * Callback for endpoint.
147 + *
148 + * @return array
149 + */
150 + protected function result() {
151 + $args = $this->query_args();
152 +
153 + if ( isset( $args['columns'] ) ) {
154 + $columns = array_map( 'trim', explode( ',', $args['columns'] ) );
155 + } else {
156 + $columns = null; // go with defaults
157 + }
158 +
159 + $store = new Replicastore();
160 +
161 + if ( ! isset( $args['strip_non_ascii'] ) ) {
162 + $args['strip_non_ascii'] = true;
163 + }
164 +
165 + /**
166 + * Hack: nullify the values of `start_id` and `end_id` if we're only requesting ranges.
167 + *
168 + * The endpoint doesn't support nullable values :(
169 + */
170 + if ( true === $args['only_range_edges'] ) {
171 + if ( 0 === $args['start_id'] ) {
172 + $args['start_id'] = null;
173 + }
174 +
175 + if ( 0 === $args['end_id'] ) {
176 + $args['end_id'] = null;
177 + }
178 + }
179 +
180 + $histogram = $store->checksum_histogram( $args['object_type'], $args['buckets'], $args['start_id'], $args['end_id'], $columns, $args['strip_non_ascii'], $args['shared_salt'], $args['only_range_edges'], $args['detailed_drilldown'] );
181 +
182 + // Hack to disable Sync during this call, so we can resolve faster.
183 + Actions::mark_sync_read_only();
184 +
185 + return array(
186 + 'histogram' => $histogram,
187 + 'type' => $store->get_checksum_type(),
188 + );
189 + }
190 +}
191 +
192 +/**
193 + * POST /sites/%s/sync/health
194 + *
195 + * @phan-constructor-used-for-side-effects
196 + */
197 +class Jetpack_JSON_API_Sync_Modify_Health_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
198 +
199 + /**
200 + * Callback for sync/health endpoint.
201 + *
202 + * @return array|WP_Error result of request.
203 + */
204 + protected function result() {
205 + $args = $this->input();
206 +
207 + switch ( $args['status'] ) {
208 + case Health::STATUS_IN_SYNC:
209 + case Health::STATUS_OUT_OF_SYNC:
210 + Health::update_status( $args['status'] );
211 + break;
212 + default:
213 + return new WP_Error( 'invalid_status', 'Invalid Sync Status Provided.' );
214 + }
215 +
216 + // re-fetch so we see what's really being stored.
217 + return array(
218 + 'success' => Health::get_status(),
219 + );
220 + }
221 +}
222 +
223 +/**
224 + * POST /sites/%s/sync/settings
225 + *
226 + * @phan-constructor-used-for-side-effects
227 + */
228 +class Jetpack_JSON_API_Sync_Modify_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
229 + /**
230 + * The endpoint callback.
231 + *
232 + * @return array
233 + */
234 + protected function result() {
235 + $args = $this->input();
236 +
237 + $sync_settings = Settings::get_settings();
238 +
239 + foreach ( $args as $key => $value ) {
240 + if ( $value !== false ) {
241 + if ( is_numeric( $value ) ) {
242 + $value = (int) $value;
243 + }
244 +
245 + // special case for sending empty arrays - a string with value 'empty'
246 + if ( $value === 'empty' ) {
247 + $value = array();
248 + }
249 +
250 + $sync_settings[ $key ] = $value;
251 + }
252 + }
253 +
254 + Settings::update_settings( $sync_settings );
255 +
256 + // re-fetch so we see what's really being stored
257 + return Settings::get_settings();
258 + }
259 +}
260 +
261 +/**
262 + * GET /sites/%s/sync/settings
263 + *
264 + * @phan-constructor-used-for-side-effects
265 + */
266 +class Jetpack_JSON_API_Sync_Get_Settings_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
267 + /**
268 + * Endpoint callback.
269 + *
270 + * @return array
271 + */
272 + protected function result() {
273 +
274 + return Settings::get_settings();
275 + }
276 +}
277 +
278 +/**
279 + * GET /sites/%s/sync/object
280 + *
281 + * @phan-constructor-used-for-side-effects
282 + */
283 +class Jetpack_JSON_API_Sync_Object extends Jetpack_JSON_API_Sync_Endpoint {
284 + /**
285 + * Endpoint callback.
286 + *
287 + * @return array|WP_Error
288 + */
289 + protected function result() {
290 + $args = $this->query_args();
291 +
292 + $module_name = $args['module_name'];
293 +
294 + $sync_module = Modules::get_module( $module_name );
295 + if ( ! $sync_module ) {
296 + return new WP_Error( 'invalid_module', 'You specified an invalid sync module' );
297 + }
298 +
299 + $object_type = $args['object_type'];
300 + $object_ids = $args['object_ids'];
301 +
302 + $codec = Sender::get_instance()->get_codec();
303 +
304 + Actions::mark_sync_read_only();
305 + Settings::set_is_syncing( true );
306 + $objects = $codec->encode( $sync_module->get_objects_by_id( $object_type, $object_ids ) );
307 + Settings::set_is_syncing( false );
308 +
309 + return array(
310 + 'objects' => $objects,
311 + 'codec' => $codec->name(),
312 + );
313 + }
314 +}
315 +
316 +/**
317 + * Sync Now endpoint class.
318 + *
319 + * @phan-constructor-used-for-side-effects
320 + */
321 +class Jetpack_JSON_API_Sync_Now_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
322 + /**
323 + * Endpoint callback.
324 + *
325 + * @return array
326 + */
327 + protected function result() {
328 + $args = $this->input();
329 + $queue_name = $this->validate_queue( $args['queue'] );
330 +
331 + if ( is_wp_error( $queue_name ) ) {
332 + return $queue_name;
333 + }
334 +
335 + $sender = Sender::get_instance();
336 + $response = $sender->do_sync_for_queue( new Queue( $args['queue'] ) );
337 +
338 + return array(
339 + 'response' => $response,
340 + );
341 + }
342 +}
343 +
344 +/**
345 + * Sync checkout endpoint.
346 + *
347 + * @phan-constructor-used-for-side-effects
348 + */
349 +class Jetpack_JSON_API_Sync_Checkout_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
350 + /**
351 + * Endpoint callback.
352 + *
353 + * @return array|WP_Error
354 + */
355 + protected function result() {
356 + $args = $this->input();
357 + $queue_name = $this->validate_queue( $args['queue'] );
358 +
359 + if ( is_wp_error( $queue_name ) ) {
360 + return $queue_name;
361 + }
362 +
363 + if ( $args['number_of_items'] < 1 || $args['number_of_items'] > 100 ) {
364 + return new WP_Error( 'invalid_number_of_items', 'Number of items needs to be an integer that is larger than 0 and less then 100', 400 );
365 + }
366 +
367 + $number_of_items = absint( $args['number_of_items'] );
368 +
369 + if ( 'immediate' === $queue_name ) {
370 + return $this->immediate_full_sync_pull( $number_of_items );
371 + }
372 +
373 + return $this->queue_pull( $queue_name, $number_of_items, $args );
374 + }
375 +
376 + /**
377 + * Create a queue.
378 + *
379 + * @param string $queue_name - the queue name.
380 + * @param int $number_of_items - the number of items.
381 + * @param array $args - the arguments.
382 + *
383 + * @return array|WP_Error
384 + */
385 + public function queue_pull( $queue_name, $number_of_items, $args ) {
386 + $queue = new Queue( $queue_name );
387 +
388 + if ( 0 === $queue->size() ) {
389 + return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
390 + }
391 +
392 + $sender = Sender::get_instance();
393 +
394 + // try to give ourselves as much time as possible.
395 + set_time_limit( 0 );
396 +
397 + if ( $args['pop'] ) {
398 + $buffer = new Queue_Buffer( 'pop', $queue->pop( $number_of_items ) );
399 + } else {
400 + // let's delete the checkin state.
401 + if ( $args['force'] ) {
402 + $queue->unlock();
403 + }
404 + $buffer = $this->get_buffer( $queue, $number_of_items );
405 + }
406 + // Check that the $buffer is not checkout out already.
407 + if ( is_wp_error( $buffer ) ) {
408 + return new WP_Error( 'buffer_open', "We couldn't get the buffer it is currently checked out", 400 );
409 + }
410 +
411 + if ( ! is_object( $buffer ) ) {
412 + return new WP_Error( 'buffer_non-object', 'Buffer is not an object', 400 );
413 + }
414 +
415 + Settings::set_is_syncing( true );
416 + list( $items_to_send, $skipped_items_ids ) = $sender->get_items_to_send( $buffer, $args['encode'] );
417 + Settings::set_is_syncing( false );
418 +
419 + return array(
420 + 'buffer_id' => $buffer->id,
421 + 'items' => $items_to_send,
422 + 'skipped_items' => $skipped_items_ids,
423 + 'codec' => $args['encode'] ? $sender->get_codec()->name() : null,
424 + 'sent_timestamp' => time(),
425 + );
426 + }
427 +
428 + /**
429 + * The items.
430 + *
431 + * @var array
432 + */
433 + public $items = array();
434 +
435 + /**
436 + * Send the data listener.
437 + */
438 + public function jetpack_sync_send_data_listener() {
439 + foreach ( func_get_args()[0] as $key => $item ) {
440 + $this->items[ $key ] = $item;
441 + }
442 + }
443 +
444 + /**
445 + * Check out a buffer of full sync actions.
446 + *
447 + * @param null $number_of_items Number of Actions to check-out.
448 + *
449 + * @return array Sync Actions to be returned to requestor
450 + */
451 + public function immediate_full_sync_pull( $number_of_items = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
452 + // try to give ourselves as much time as possible.
453 + set_time_limit( 0 );
454 +
455 + $original_send_data_cb = array( 'Automattic\Jetpack\Sync\Actions', 'send_data' );
456 + $temp_send_data_cb = array( $this, 'jetpack_sync_send_data_listener' );
457 +
458 + Sender::get_instance()->set_enqueue_wait_time( 0 );
459 + remove_filter( 'jetpack_sync_send_data', $original_send_data_cb );
460 + add_filter( 'jetpack_sync_send_data', $temp_send_data_cb, 10, 6 );
461 + Sender::get_instance()->do_full_sync();
462 + remove_filter( 'jetpack_sync_send_data', $temp_send_data_cb );
463 + add_filter( 'jetpack_sync_send_data', $original_send_data_cb, 10, 6 );
464 +
465 + return array(
466 + 'items' => $this->items,
467 + 'codec' => Sender::get_instance()->get_codec()->name(),
468 + 'sent_timestamp' => time(),
469 + 'status' => Actions::get_sync_status(),
470 + );
471 + }
472 +
473 + /**
474 + * Get the queue buffer.
475 + *
476 + * @param object $queue - the queue.
477 + * @param int $number_of_items - the number of items.
478 + *
479 + * @return Automattic\Jetpack\Sync\Queue_Buffer|bool|int|\WP_Error
480 + */
481 + protected function get_buffer( $queue, $number_of_items ) {
482 + $start = time();
483 + $max_duration = 5; // this will try to get the buffer
484 +
485 + $buffer = $queue->checkout( $number_of_items );
486 + $duration = time() - $start;
487 +
488 + while ( is_wp_error( $buffer ) && $duration < $max_duration ) {
489 + sleep( 2 );
490 + $duration = time() - $start;
491 + $buffer = $queue->checkout( $number_of_items );
492 + }
493 +
494 + if ( $buffer === false ) {
495 + return new WP_Error( 'queue_size', 'The queue is empty and there is nothing to send', 400 );
496 + }
497 +
498 + return $buffer;
499 + }
500 +}
501 +
502 +/**
503 + * Close endpoint class.
504 + *
505 + * @phan-constructor-used-for-side-effects
506 + */
507 +class Jetpack_JSON_API_Sync_Close_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
508 + /**
509 + * Endpoint callback.
510 + *
511 + * @return array|WP_Error
512 + */
513 + protected function result() {
514 +
515 + $request_body = $this->input();
516 + $queue_name = $this->validate_queue( $request_body['queue'] );
517 +
518 + if ( is_wp_error( $queue_name ) ) {
519 + return $queue_name;
520 + }
521 +
522 + if ( ! isset( $request_body['buffer_id'] ) ) {
523 + return new WP_Error( 'missing_buffer_id', 'Please provide a buffer id', 400 );
524 + }
525 +
526 + if ( ! isset( $request_body['item_ids'] ) || ! is_array( $request_body['item_ids'] ) ) {
527 + return new WP_Error( 'missing_item_ids', 'Please provide a list of item ids in the item_ids argument', 400 );
528 + }
529 +
530 + $request_body['buffer_id'] = preg_replace( '/[^A-Za-z0-9]/', '', $request_body['buffer_id'] );
531 + $request_body['item_ids'] = array_filter( array_map( array( 'Jetpack_JSON_API_Sync_Close_Endpoint', 'sanitize_item_ids' ), $request_body['item_ids'] ) );
532 +
533 + $queue = new Queue( $queue_name );
534 +
535 + $items = $queue->peek_by_id( $request_body['item_ids'] );
536 +
537 + // Update Full Sync Status if queue is "full_sync".
538 + if ( 'full_sync' === $queue_name ) {
539 + $full_sync_module = Modules::get_module( 'full-sync' );
540 + '@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $full_sync_module';
541 +
542 + $full_sync_module->update_sent_progress_action( $items );
543 + }
544 +
545 + $buffer = new Queue_Buffer( $request_body['buffer_id'], $request_body['item_ids'] );
546 + $response = $queue->close( $buffer, $request_body['item_ids'] );
547 +
548 + // Perform another checkout?
549 + if ( isset( $request_body['continue'] ) && $request_body['continue'] ) {
550 + if ( in_array( $queue_name, array( 'full_sync', 'immediate' ), true ) ) {
551 + // Send Full Sync Actions.
552 + Sender::get_instance()->do_full_sync();
553 + } elseif ( $queue->has_any_items() ) {
554 + // Send Incremental Sync Actions.
555 + Sender::get_instance()->do_sync();
556 + }
557 + }
558 +
559 + if ( is_wp_error( $response ) ) {
560 + return $response;
561 + }
562 +
563 + return array(
564 + 'success' => $response,
565 + 'status' => Actions::get_sync_status(),
566 + );
567 + }
568 +
569 + /**
570 + * Sanitize item IDs.
571 + *
572 + * @param string $item - the item we're sanitizing.
573 + *
574 + * @return string|null
575 + */
576 + protected static function sanitize_item_ids( $item ) {
577 + // lets not delete any options that don't start with jpsq_sync-
578 + if ( ! is_string( $item ) || ! str_starts_with( $item, 'jpsq_' ) ) {
579 + return null;
580 + }
581 + // Limit to A-Z,a-z,0-9,_,-,.
582 + return preg_replace( '/[^A-Za-z0-9-_.]/', '', $item );
583 + }
584 +}
585 +
586 +/**
587 + * Unlock endpoint class.
588 + *
589 + * @phan-constructor-used-for-side-effects
590 + */
591 +class Jetpack_JSON_API_Sync_Unlock_Endpoint extends Jetpack_JSON_API_Sync_Endpoint {
592 + /**
593 + * Endpoint callback.
594 + *
595 + * @return array|WP_Error
596 + */
597 + protected function result() {
598 + $args = $this->input();
599 +
600 + if ( ! isset( $args['queue'] ) ) {
601 + return new WP_Error( 'invalid_queue', 'Queue name is required', 400 );
602 + }
603 +
604 + if ( ! in_array( $args['queue'], array( 'sync', 'full_sync' ), true ) ) {
605 + return new WP_Error( 'invalid_queue', 'Queue name should be sync or full_sync', 400 );
606 + }
607 +
608 + $queue = new Queue( $args['queue'] );
609 +
610 + // False means that there was no lock to delete.
611 + $response = $queue->unlock();
612 + return array(
613 + 'success' => $response,
614 + );
615 + }
616 +}
617 +
618 +/**
619 + * Object ID range class.
620 + *
621 + * @phan-constructor-used-for-side-effects
622 + */
623 +class Jetpack_JSON_API_Sync_Object_Id_Range extends Jetpack_JSON_API_Sync_Endpoint {
624 + /**
625 + * Endpoint callback.
626 + *
627 + * @return array|WP_Error
628 + */
629 + protected function result() {
630 + $args = $this->query_args();
631 +
632 + $module_name = $args['sync_module'];
633 + $batch_size = $args['batch_size'];
634 +
635 + if ( ! $this->is_valid_sync_module( $module_name ) ) {
636 + return new WP_Error( 'invalid_module', 'This sync module cannot be used to calculate a range.', 400 );
637 + }
638 +
639 + $module = Modules::get_module( $module_name );
640 +
641 + return array(
642 + 'ranges' => $module->get_min_max_object_ids_for_batches( $batch_size ),
643 + );
644 + }
645 +
646 + /**
647 + * Check if sync module is valid.
648 + *
649 + * @param string $module_name - the module name.
650 + *
651 + * @return bool
652 + */
653 + protected function is_valid_sync_module( $module_name ) {
654 + return in_array(
655 + $module_name,
656 + array(
657 + 'comments',
658 + 'posts',
659 + 'terms',
660 + 'term_relationships',
661 + 'users',
662 + ),
663 + true
664 + );
665 + }
666 +}