PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules / class-full-sync-immediately.php

class-full-sync-immediately.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-sync/src/modules/class-full-sync-immediately.php

580 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
580