PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/modules/class-full-sync-immediately.php +579 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,579 @@
1 +<?php
2 +/**
3 + * Full sync module.
4 + *
5 + * @package automattic/jetpack-sync
6 + */
7 +
8 +namespace Automattic\Jetpack\Sync\Modules;
9 +
10 +use Automattic\Jetpack\Sync\Actions;
11 +use Automattic\Jetpack\Sync\Defaults;
12 +use Automattic\Jetpack\Sync\Lock;
13 +use Automattic\Jetpack\Sync\Modules;
14 +use Automattic\Jetpack\Sync\Settings;
15 +
16 +if ( ! defined( 'ABSPATH' ) ) {
17 + exit( 0 );
18 +}
19 +
20 +/**
21 + * This class does a full resync of the database by
22 + * sending an outbound action for every single object
23 + * that we care about.
24 + */
25 +class Full_Sync_Immediately extends Module {
26 + /**
27 + * Prefix of the full sync status option name.
28 + *
29 + * @var string
30 + */
31 + const STATUS_OPTION = 'jetpack_sync_full_status';
32 +
33 + /**
34 + * Sync Lock name.
35 + *
36 + * @var string
37 + */
38 + const LOCK_NAME = 'full_sync';
39 +
40 + /**
41 + * Sync module name.
42 + *
43 + * @access public
44 + *
45 + * @return string
46 + */
47 + public function name() {
48 + return 'full-sync';
49 + }
50 +
51 + /**
52 + * Initialize action listeners for full sync.
53 + *
54 + * @access public
55 + *
56 + * @param callable $callable Action handler callable.
57 + */
58 + public function init_full_sync_listeners( $callable ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
59 + }
60 +
61 + /**
62 + * Start a full sync.
63 + *
64 + * @access public
65 + *
66 + * @param array $full_sync_config Full sync configuration.
67 + * @param mixed $context The context where the full sync was initiated from.
68 + *
69 + * @return bool Always returns true at success.
70 + */
71 + public function start( $full_sync_config = null, $context = null ) {
72 + // Check if there was a full sync in progress already before resetting the data.
73 + $should_process_cancelled_action = $this->get_status()['start_action_processed'] && ! $this->is_finished();
74 + // Remove all evidence of previous full sync items and status.
75 + $this->reset_data();
76 +
77 + // Update status to indicate that a new full sync is starting and need to cancel previous one.
78 + if ( $should_process_cancelled_action ) {
79 + $this->update_status(
80 + array(
81 + 'cancelled_action_processed' => false,
82 + )
83 + );
84 + }
85 +
86 + if ( ! is_array( $full_sync_config ) ) {
87 + /*
88 + * Filter default sync config to allow injecting custom configuration.
89 + *
90 + * @param array $full_sync_config Sync configuration for all sync modules.
91 + *
92 + * @since 3.10.0
93 + */
94 + $full_sync_config = apply_filters( 'jetpack_full_sync_config', Defaults::$default_full_sync_config );
95 + if ( is_multisite() ) {
96 + $full_sync_config['network_options'] = 1;
97 + }
98 + }
99 +
100 + if ( isset( $full_sync_config['users'] ) && 'initial' === $full_sync_config['users'] ) {
101 + $users_module = Modules::get_module( 'users' );
102 + '@phan-var Users $users_module';
103 + $full_sync_config['users'] = $users_module->get_initial_sync_user_config();
104 + }
105 +
106 + $this->update_status(
107 + array(
108 + 'started' => time(),
109 + 'config' => $full_sync_config,
110 + 'context' => $context,
111 + )
112 + );
113 +
114 + return true;
115 + }
116 +
117 + /**
118 + * Whether full sync has started.
119 + *
120 + * @access public
121 + *
122 + * @return boolean
123 + */
124 + public function is_started() {
125 + return (bool) $this->get_status()['started'];
126 + }
127 +
128 + /**
129 + * Retrieve the status of the current full sync.
130 + *
131 + * @access public
132 + *
133 + * @return array Full sync status.
134 + */
135 + public function get_status() {
136 + $default = array(
137 + 'start_action_processed' => false,
138 + 'cancelled_action_processed' => true, // true by default to avoid sending the action when there is no need,
139 + 'started' => false,
140 + 'finished' => false,
141 + 'progress' => array(),
142 + 'config' => array(),
143 + 'context' => null,
144 + );
145 +
146 + return wp_parse_args( \Jetpack_Options::get_raw_option( self::STATUS_OPTION ), $default );
147 + }
148 +
149 + /**
150 + * Returns the progress percentage of a full sync.
151 + *
152 + * @access public
153 + *
154 + * @return int|null
155 + */
156 + public function get_sync_progress_percentage() {
157 + if ( ! $this->is_started() || $this->is_finished() ) {
158 + return null;
159 + }
160 + $status = $this->get_status();
161 + if ( empty( $status['progress'] ) ) {
162 + return null;
163 + }
164 + $total_items = array_reduce(
165 + array_values( $status['progress'] ),
166 + function ( $sum, $sync_item ) {
167 + return isset( $sync_item['total'] ) ? ( $sum + (int) $sync_item['total'] ) : $sum;
168 + },
169 + 0
170 + );
171 + $total_sent = array_reduce(
172 + array_values( $status['progress'] ),
173 + function ( $sum, $sync_item ) {
174 + return isset( $sync_item['sent'] ) ? ( $sum + (int) $sync_item['sent'] ) : $sum;
175 + },
176 + 0
177 + );
178 + return floor( ( $total_sent / $total_items ) * 100 );
179 + }
180 +
181 + /**
182 + * Whether full sync has finished.
183 + *
184 + * @access public
185 + *
186 + * @return boolean
187 + */
188 + public function is_finished() {
189 + return (bool) $this->get_status()['finished'];
190 + }
191 +
192 + /**
193 + * Clear all the full sync data.
194 + *
195 + * @access public
196 + */
197 + public function reset_data() {
198 + $this->clear_status();
199 + ( new Lock() )->remove( self::LOCK_NAME, true );
200 + }
201 +
202 + /**
203 + * Clear all the full sync status options.
204 + *
205 + * @access public
206 + */
207 + public function clear_status() {
208 + \Jetpack_Options::delete_raw_option( self::STATUS_OPTION );
209 + }
210 +
211 + /**
212 + * Updates the status of the current full sync.
213 + *
214 + * @access public
215 + *
216 + * @param array $values New values to set.
217 + *
218 + * @return bool True if success.
219 + */
220 + public function update_status( $values ) {
221 + return $this->set_status( wp_parse_args( $values, $this->get_status() ) );
222 + }
223 +
224 + /**
225 + * Retrieve the status of the current full sync.
226 + *
227 + * @param array $values New values to set.
228 + *
229 + * @access public
230 + *
231 + * @return boolean Full sync status.
232 + */
233 + public function set_status( $values ) {
234 + return \Jetpack_Options::update_raw_option( self::STATUS_OPTION, $values );
235 + }
236 +
237 + /**
238 + * Given an initial Full Sync configuration get the initial status.
239 + *
240 + * @param array $full_sync_config Full sync configuration.
241 + * @param array $range Range of the sync items, containing min, max and count IDs for some item types.
242 + *
243 + * @return array Initial Sent status.
244 + */
245 + public function get_initial_progress( $full_sync_config, $range = null ) {
246 + // Set default configuration, calculate totals, and save configuration if totals > 0.
247 + $status = array();
248 + foreach ( $full_sync_config as $name => $config ) {
249 + $module = Modules::get_module( $name );
250 + if ( ! $module ) {
251 + continue;
252 + }
253 + $status[ $name ] = array(
254 + // If we have a range for the module, use the count from the range to avoid querying the database again.
255 + 'total' => $range[ $name ]->count ?? $module->total( $config ),
256 + 'sent' => 0,
257 + 'finished' => false,
258 + );
259 + }
260 +
261 + return $status;
262 + }
263 +
264 + /**
265 + * Get the range for content (posts and comments) to sync.
266 + *
267 + * @access private
268 + *
269 + * @param array $full_sync_config Full sync configuration.
270 + *
271 + * @return array Array of range (min ID, max ID, total items) for all content types.
272 + */
273 + private function get_content_range( $full_sync_config ) {
274 + $range = array();
275 + foreach ( $full_sync_config as $module_name => $config ) {
276 + // Calculate ranges only for modules that get chunked.
277 + if ( in_array( $module_name, array( 'constants', 'functions', 'network_options', 'options', 'themes', 'updates' ), true ) ) {
278 + continue;
279 + }
280 + $module = Modules::get_module( $module_name );
281 + if ( ! $module ) {
282 + continue;
283 + }
284 + if ( $config ) {
285 + $range[ $module_name ] = $this->get_range( $module_name );
286 + }
287 + }
288 +
289 + return $range;
290 + }
291 +
292 + /**
293 + * Get the range (min ID, max ID and total items) of items to sync.
294 + *
295 + * @access public
296 + *
297 + * @param string $type Type of sync item to get the range for.
298 + *
299 + * @return array Array of min ID, max ID and total items in the range.
300 + */
301 + public function get_range( $type ) {
302 + global $wpdb;
303 + $module = Modules::get_module( $type );
304 + if ( ! $module ) {
305 + return array();
306 + }
307 +
308 + $table = $module->table();
309 + $id = $module->id_field();
310 + if ( 'terms' === $module ) { // Terms module relies on the term_taxonomy and term_taxonomy_id for the where sql, let's use term_id instead.
311 + $id = 'term_id';
312 + }
313 + $where_sql = $module->get_where_sql( array() );
314 +
315 + // TODO: Call $wpdb->prepare on the following query.
316 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
317 + $results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
318 + if ( isset( $results[0] ) ) {
319 + return $results[0];
320 + }
321 +
322 + return array();
323 + }
324 +
325 + /**
326 + * Continue sending instead of enqueueing.
327 + *
328 + * @access public
329 + */
330 + public function continue_enqueuing() {
331 + $this->continue_sending();
332 + }
333 +
334 + /**
335 + * Continue sending.
336 + *
337 + * @access public
338 + */
339 + public function continue_sending() {
340 + // Return early if Full Sync is not running.
341 + if ( ! $this->is_started() || $this->get_status()['finished'] ) {
342 + return;
343 + }
344 +
345 + // Return early if we've gotten a retry-after header response.
346 + $retry_time = get_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send' );
347 + if ( $retry_time ) {
348 + // If expired delete but don't send. Send will occurr in new request to avoid race conditions.
349 + if ( microtime( true ) > $retry_time ) {
350 + update_option( Actions::RETRY_AFTER_PREFIX . 'immediate-send', false, false );
351 + }
352 + return false;
353 + }
354 +
355 + // Obtain send Lock.
356 + $lock = new Lock();
357 + $lock_expiration = $lock->attempt( self::LOCK_NAME );
358 +
359 + // Return if unable to obtain lock.
360 + if ( false === $lock_expiration ) {
361 + return;
362 + }
363 +
364 + // Send Full Sync actions.
365 + $success = $this->send();
366 +
367 + // Remove lock.
368 + if ( $success ) {
369 + $lock->remove( self::LOCK_NAME, $lock_expiration );
370 + }
371 + }
372 +
373 + /**
374 + * Immediately send the next items to full sync.
375 + *
376 + * @access public
377 + */
378 + public function send() {
379 +
380 + if ( ! $this->maybe_send_cancelled_action() ) {
381 + return false;
382 + }
383 +
384 + if ( ! $this->maybe_send_full_sync_start() ) {
385 + return false;
386 + }
387 + $config = $this->get_status()['config'];
388 +
389 + $max_duration = Settings::get_setting( 'full_sync_send_duration' );
390 + $send_until = microtime( true ) + $max_duration;
391 +
392 + $progress = $this->get_status()['progress'];
393 +
394 + $started = $this->get_status()['started'];
395 +
396 + $remaining_modules = $this->get_remaining_modules_to_send();
397 +
398 + foreach ( $remaining_modules as $module ) {
399 + $module_name = $module->name();
400 + if ( array_key_exists( $module_name, $progress ) && array_key_exists( $module_name, $config ) ) {
401 + $progress[ $module_name ] = $module->send_full_sync_actions( $config[ $module_name ], $progress[ $module_name ], $send_until, $started );
402 + if ( isset( $progress[ $module_name ]['error'] ) ) {
403 + unset( $progress[ $module_name ]['error'] );
404 + $this->update_status( array( 'progress' => $progress ) );
405 + return false;
406 + } elseif ( ! $progress[ $module_name ]['finished'] ) {
407 + $this->update_status( array( 'progress' => $progress ) );
408 + return true;
409 + }
410 + }
411 +
412 + if ( $this->get_status()['started'] !== $started ) {
413 + // Full sync was restarted, stop sending.
414 + return false;
415 + }
416 + }
417 +
418 + // Check that all remaining modules in progress are actually finished.
419 + // If a module was skipped in the main loop (due to being unfinished), but still exists in progress, we shouldn't mark the sync as complete.
420 + foreach ( $remaining_modules as $module ) {
421 + $name = $module->name();
422 + if ( array_key_exists( $name, $progress ) && empty( $progress[ $name ]['finished'] ) ) {
423 + $this->update_status( array( 'progress' => $progress ) );
424 + return true;
425 + }
426 + }
427 +
428 + $this->send_full_sync_end();
429 + $this->update_status( array( 'progress' => $progress ) );
430 + return true;
431 + }
432 +
433 + /**
434 + * Get Modules that are configured to Full Sync and haven't finished sending
435 + *
436 + * @return array
437 + */
438 + public function get_remaining_modules_to_send() {
439 + $status = $this->get_status();
440 + $remaining_modules = array();
441 + foreach ( array_keys( $status['config'] ) as $module_name ) {
442 + $module = Modules::get_module( $module_name );
443 + if ( ! $module ) {
444 + continue;
445 + }
446 + if ( isset( $status['progress'][ $module_name ]['finished'] ) &&
447 + true === $status['progress'][ $module_name ]['finished'] ) {
448 + continue;
449 + }
450 + // Ensure that 'constants', 'options', and 'callables' are sent first.
451 + if ( in_array( $module_name, array( 'network_options', 'options', 'functions', 'constants' ), true ) ) {
452 + array_unshift( $remaining_modules, $module );
453 + } else {
454 + $remaining_modules[] = $module;
455 + }
456 + }
457 + return $remaining_modules;
458 + }
459 +
460 + /**
461 + * Sends the `jetpack_full_sync_start` action if it hasn't been processed yet.
462 + *
463 + * Prepares the full sync start action, sends it to WordPress.com, fires the local action,
464 + * and updates the sync status to reflect that the start action has been processed.
465 + *
466 + * @return bool True if the action was successfully sent or already processed, false on failure.
467 + */
468 + private function maybe_send_full_sync_start() {
469 + $status = $this->get_status();
470 +
471 + // If already processed, nothing to do.
472 + if ( true === $status['start_action_processed'] ) {
473 + return true;
474 + }
475 +
476 + $config = $status['config'];
477 + $context = $status['context'];
478 + $range = $this->get_content_range( $config );
479 +
480 + $result = $this->send_action( 'jetpack_full_sync_start', array( $config, $range, $context ) );
481 +
482 + // If the action failed on WordPress.com, return false.
483 + if ( is_wp_error( $result ) ) {
484 + return false;
485 + }
486 +
487 + /**
488 + * Fires when a full sync begins. This action is serialized
489 + * and sent to the server so that it knows a full sync is coming.
490 + *
491 + * @param array $config Sync configuration for all sync modules.
492 + * @param array $range Range of the sync items, containing min and max IDs for some item types.
493 + * @param mixed $context The context where the full sync was initiated from.
494 + *
495 + * @since 1.6.3
496 + * @since-jetpack 4.2.0
497 + * @since-jetpack 7.3.0 Added $range arg.
498 + * @since 4.4.0 Added $context arg.
499 + */
500 + do_action( 'jetpack_full_sync_start', $config, $range );
501 +
502 + $this->update_status(
503 + array(
504 + 'start_action_processed' => true,
505 + 'progress' => $this->get_initial_progress( $config, $range ),
506 + )
507 + );
508 +
509 + return true;
510 + }
511 +
512 + /**
513 + * Sends the `jetpack_full_sync_cancelled` action if it hasn't been processed yet.
514 + *
515 + * @return bool True if the action was successfully sent or already processed, false on failure.
516 + */
517 + private function maybe_send_cancelled_action() {
518 + $status = $this->get_status();
519 +
520 + if ( true === $status['cancelled_action_processed'] ) {
521 + return true;
522 + }
523 +
524 + $result = $this->send_action( 'jetpack_full_sync_cancelled' );
525 +
526 + if ( is_wp_error( $result ) ) {
527 + return false;
528 + }
529 +
530 + /**
531 + * Fires when a full sync is cancelled.
532 + *
533 + * @since 1.6.3
534 + * @since-jetpack 4.2.0
535 + */
536 + do_action( 'jetpack_full_sync_cancelled' );
537 + $this->update_status( array( 'cancelled_action_processed' => true ) );
538 + return true;
539 + }
540 +
541 + /**
542 + * Sends the `jetpack_full_sync_end` action and updates the status when the full sync end action is processed.
543 + *
544 + * @access public
545 + */
546 + public function send_full_sync_end() {
547 + $status = $this->get_status();
548 + $range = $this->get_content_range( $status['config'] );
549 + $context = $status['context'];
550 +
551 + $result = $this->send_action( 'jetpack_full_sync_end', array( '', $range, $context ) );
552 +
553 + if ( is_wp_error( $result ) ) { // Do not set finished status if we get an error.
554 + return;
555 + }
556 + /**
557 + * Fires when a full sync ends. This action is serialized
558 + * and sent to the server.
559 + *
560 + * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
561 + * @param array $range Range of the sync items, containing min and max IDs for some item types.
562 + *
563 + * @since 1.6.3
564 + * @since-jetpack 4.2.0
565 + * @since-jetpack 7.3.0 Added $range arg.
566 + */
567 + do_action( 'jetpack_full_sync_end', '', $range );
568 +
569 + // Setting autoload to true means that it's faster to check whether we should continue enqueuing.
570 + $this->update_status( array( 'finished' => time() ) );
571 + }
572 +
573 + /**
574 + * Empty Function as we don't close buffers on Immediate Full Sync.
575 + *
576 + * @param array $actions an array of actions, ignored for queueless sync.
577 + */
578 + public function update_sent_progress_action( $actions ) { } // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
579 +}