PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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
← All changes | jetpack_vendor/automattic/jetpack-sync/src/modules/class-module.php +260 -41 13.1.5 → 16.3-a.7 View file →
@@ -6,8 +6,9 @@
6 6 */
7 7
8 8 namespace Automattic\Jetpack\Sync\Modules;
9 9
10 +use Automattic\Jetpack\Sync\Defaults;
10 11 use Automattic\Jetpack\Sync\Functions;
11 12 use Automattic\Jetpack\Sync\Listener;
12 13 use Automattic\Jetpack\Sync\Replicastore;
13 14 use Automattic\Jetpack\Sync\Sender;
@@ -12,8 +13,12 @@
12 13 use Automattic\Jetpack\Sync\Replicastore;
13 14 use Automattic\Jetpack\Sync\Sender;
14 15 use Automattic\Jetpack\Sync\Settings;
15 16
17 +if ( ! defined( 'ABSPATH' ) ) {
18 + exit( 0 );
19 +}
20 +
16 21 /**
17 22 * Basic methods implemented by Jetpack Sync extensions.
18 23 *
19 24 * @abstract
@@ -28,8 +33,37 @@
28 33 */
29 34 const ARRAY_CHUNK_SIZE = 10;
30 35
31 36 /**
37 + * Max query length for DB queries.
38 + *
39 + * @access public
40 + *
41 + * @var int
42 + */
43 + const MAX_DB_QUERY_LENGTH = 15 * 1024;
44 +
45 + /**
46 + * Max bytes allowed for full sync upload for the module.
47 + * Default Setting : 7MB.
48 + *
49 + * @access public
50 + *
51 + * @var int
52 + */
53 + const MAX_SIZE_FULL_SYNC = 7000000;
54 +
55 + /**
56 + * Max bytes allowed for post meta_value => length.
57 + * Default Setting : 2MB.
58 + *
59 + * @access public
60 + *
61 + * @var int
62 + */
63 + const MAX_META_LENGTH = 2000000;
64 +
65 + /**
32 66 * Sync module name.
33 67 *
34 68 * @access public
35 69 *
@@ -48,18 +82,42 @@
48 82 return 'ID';
49 83 }
50 84
51 85 /**
52 - * The table in the database.
86 + * The table name.
53 87 *
54 88 * @access public
55 89 *
56 90 * @return string|bool
91 + * @deprecated since 3.11.0 Use table() instead.
57 92 */
58 93 public function table_name() {
94 + _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\Module->table' );
59 95 return false;
60 96 }
61 97
98 + /**
99 + * The table in the database with the prefix.
100 + *
101 + * @access public
102 + *
103 + * @return string|bool
104 + */
105 + public function table() {
106 + return false;
107 + }
108 +
109 + /**
110 + * The full sync action name for this module.
111 + *
112 + * @access public
113 + *
114 + * @return string
115 + */
116 + public function full_sync_action_name() {
117 + return 'jetpack_full_sync_' . $this->name();
118 + }
119 +
62 120 // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
63 121
64 122 /**
65 123 * Retrieve a sync object by its ID.
@@ -140,9 +198,9 @@
140 198 *
141 199 * @access public
142 200 *
143 201 * @param array $config Full sync configuration for this sync module.
144 - * @return array Number of items yet to be enqueued.
202 + * @return int Number of items yet to be enqueued.
145 203 */
146 204 public function estimate_full_sync_actions( $config ) {
147 205 // In subclasses, return the number of items yet to be enqueued.
148 206 return null;
@@ -187,9 +245,14 @@
187 245 // Associative array order changes the generated checksum value.
188 246 if ( $sort && is_array( $values ) ) {
189 247 $this->recursive_ksort( $values );
190 248 }
191 - return crc32( wp_json_encode( Functions::json_wrap( $values ) ) );
249 + return crc32(
250 + wp_json_encode(
251 + Functions::json_wrap( $values ),
252 + 0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because we don't want disrupt the checksum algorithm.
253 + )
254 + );
192 255 }
193 256
194 257 /**
195 258 * Recursively call ksort on an Array
@@ -292,21 +355,21 @@
292 355 *
293 356 * @return array|object|null
294 357 */
295 358 public function get_next_chunk( $config, $status, $chunk_size ) {
296 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
359 + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
297 360 global $wpdb;
298 361 return $wpdb->get_col(
299 - <<<SQL
300 -SELECT {$this->id_field()}
301 -FROM {$wpdb->{$this->table_name()}}
302 -WHERE {$this->get_where_sql( $config )}
303 -AND {$this->id_field()} < {$status['last_sent']}
304 -ORDER BY {$this->id_field()}
305 -DESC LIMIT {$chunk_size}
306 -SQL
362 + "
363 + SELECT {$this->id_field()}
364 + FROM {$this->table()}
365 + WHERE {$this->get_where_sql( $config )}
366 + AND {$this->id_field()} < {$status['last_sent']}
367 + ORDER BY {$this->id_field()}
368 + DESC LIMIT {$chunk_size}
369 + "
307 370 );
308 - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
371 + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
309 372 }
310 373
311 374 /**
312 375 * Return last_item to send for Module Full Sync Configuration.
@@ -318,15 +381,15 @@
318 381 public function get_last_item( $config ) {
319 382 global $wpdb;
320 383 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
321 384 return $wpdb->get_var(
322 - <<<SQL
323 -SELECT {$this->id_field()}
324 -FROM {$wpdb->{$this->table_name()}}
325 -WHERE {$this->get_where_sql( $config )}
326 -ORDER BY {$this->id_field()}
327 -LIMIT 1
328 -SQL
385 + "
386 + SELECT {$this->id_field()}
387 + FROM {$this->table()}
388 + WHERE {$this->get_where_sql( $config )}
389 + ORDER BY {$this->id_field()}
390 + LIMIT 1
391 + "
329 392 );
330 393 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
331 394 }
332 395
@@ -343,15 +406,16 @@
343 406 * Immediately send all items of a sync type as an action.
344 407 *
345 408 * @access protected
346 409 *
347 - * @param string $config Full sync configuration for this module.
348 - * @param array $status the current module full sync status.
349 - * @param float $send_until timestamp until we want this request to send full sync events.
410 + * @param array $config Full sync configuration for this module.
411 + * @param array $status the current module full sync status.
412 + * @param float $send_until timestamp until we want this request to send full sync events.
413 + * @param int $started The timestamp when the full sync started.
350 414 *
351 415 * @return array Status, the module full sync status updated.
352 416 */
353 - public function send_full_sync_actions( $config, $status, $send_until ) {
417 + public function send_full_sync_actions( $config, $status, $send_until, $started ) {
354 418 global $wpdb;
355 419
356 420 if ( empty( $status['last_sent'] ) ) {
357 421 $status['last_sent'] = $this->get_initial_last_sent();
@@ -356,13 +420,30 @@
356 420 if ( empty( $status['last_sent'] ) ) {
357 421 $status['last_sent'] = $this->get_initial_last_sent();
358 422 }
359 423
360 - $limits = Settings::get_setting( 'full_sync_limits' )[ $this->name() ];
424 + $limits = Settings::get_setting( 'full_sync_limits' )[ $this->name() ] ??
425 + Defaults::get_default_setting( 'full_sync_limits' )[ $this->name() ] ??
426 + array(
427 + 'max_chunks' => null,
428 + 'chunk_size' => null,
429 + );
361 430
431 + $limits = array(
432 + 'max_chunks' => is_numeric( $limits['max_chunks'] ) ? (int) $limits['max_chunks'] : 10,
433 + 'chunk_size' => is_numeric( $limits['chunk_size'] ) ? (int) $limits['chunk_size'] : 100,
434 + );
435 +
436 + $limits['chunk_size'] = $this->adjust_chunk_size_if_stuck( $status['last_sent'], $limits['chunk_size'], $started );
437 +
362 438 $chunks_sent = 0;
363 439
364 - $last_item = $this->get_last_item( $config );
440 + // Store last_item in status to avoid re-running this expensive query on every invocation.
441 + // The minimum ID does not change during a Full Sync.
442 + if ( ! isset( $status['last_item'] ) ) {
443 + $status['last_item'] = $this->get_last_item( $config );
444 + }
445 + $last_item = $status['last_item'];
365 446
366 447 while ( $chunks_sent < $limits['max_chunks'] && microtime( true ) < $send_until ) {
367 448 $objects = $this->get_next_chunk( $config, $status, $limits['chunk_size'] );
368 449
@@ -374,13 +455,20 @@
374 455 if ( empty( $objects ) ) {
375 456 $status['finished'] = true;
376 457 return $status;
377 458 }
378 - $result = $this->send_action( 'jetpack_full_sync_' . $this->name(), array( $objects, $status['last_sent'] ) );
379 - if ( is_wp_error( $result ) || $wpdb->last_error ) {
380 - $status['error'] = true;
381 - return $status;
459 + // If we have objects as a key it means get_next_chunk is being overridden, we need to check for it being an empty array.
460 + // In case it is an empty array, we should not send the action or increase the chunks_sent, we just need to update the status.
461 + if ( ! isset( $objects['objects'] ) || array() !== $objects['objects'] ) {
462 + $key = $this->full_sync_action_name() . '_' . crc32( wp_json_encode( $status['last_sent'], JSON_UNESCAPED_SLASHES ) );
463 + $result = $this->send_action( $this->full_sync_action_name(), array( $objects, $status['last_sent'] ), $key );
464 + if ( is_wp_error( $result ) || $wpdb->last_error ) {
465 + $status['error'] = true;
466 + return $status;
467 + }
468 + ++$chunks_sent;
382 469 }
470 +
383 471 // Updated the sent and last_sent status.
384 472 $status = $this->set_send_full_sync_actions_status( $status, $objects );
385 473 if ( $last_item === $status['last_sent'] ) {
386 474 $status['finished'] = true;
@@ -385,9 +473,8 @@
385 473 if ( $last_item === $status['last_sent'] ) {
386 474 $status['finished'] = true;
387 475 return $status;
388 476 }
389 - ++$chunks_sent;
390 477 }
391 478
392 479 return $status;
393 480 }
@@ -392,9 +479,78 @@
392 479 return $status;
393 480 }
394 481
395 482 /**
483 + * Adjust chunk size using adaptive logic and update transient for tracking stuck state.
484 + *
485 + * @param string $last_sent The current last_sent marker.
486 + * @param int $default_chunk_size The default chunk size.
487 + * @param int $started The timestamp when the full sync started.
488 + * @return int Adjusted chunk size.
489 + */
490 + private function adjust_chunk_size_if_stuck( $last_sent, $default_chunk_size, $started ) {
491 + $transient_key = 'jetpack_sync_last_sent_' . $this->name() . '_' . $started;
492 + $stuck_data = get_transient( $transient_key );
493 + $is_stuck = isset( $stuck_data['last_sent'] ) && $stuck_data['last_sent'] === $last_sent;
494 +
495 + // Preserve the adjusted chunk size and stuck count from the transient when stuck.
496 + $stuck_count = $is_stuck && isset( $stuck_data['stuck_count'] ) ? $stuck_data['stuck_count'] : 0;
497 + $adjusted_chunk_size = $is_stuck && isset( $stuck_data['adjusted_chunk_size'] ) ? $stuck_data['adjusted_chunk_size'] : $default_chunk_size;
498 +
499 + if ( $is_stuck && $stuck_data['adjusted_chunk_size'] === 1 ) {
500 + // Refresh transient TTL to prevent expiry-driven reset cycles.
501 + set_transient( $transient_key, $stuck_data, HOUR_IN_SECONDS );
502 + return 1; // If we are already at the minimum chunk size, do not adjust further.
503 + }
504 +
505 + // We will adjust if it is stuck after 10 minutes.
506 + if (
507 + $is_stuck &&
508 + ( time() - $stuck_data['timestamp'] ) >= 10 * MINUTE_IN_SECONDS
509 + ) {
510 + ++$stuck_count;
511 + $adjusted_chunk_size = max( 1, (int) ( $default_chunk_size / ( 2 ** $stuck_count ) ) ); // Halve the chunk size for each stuck iteration.
512 +
513 + // Send one HTTP notification when chunk size reaches the minimum (1)
514 + // so the stuck state is visible for monitoring. Intermediate cascade
515 + // steps skip the HTTP request to avoid consuming the time budget.
516 + if ( 1 === $adjusted_chunk_size ) {
517 + $this->send_action(
518 + 'jetpack_full_sync_stuck_adjustment',
519 + array(
520 + 'module' => $this->name(),
521 + 'last_sent' => $last_sent,
522 + 'stuck_count' => $stuck_count,
523 + 'adjusted_chunk_size' => $adjusted_chunk_size,
524 + )
525 + );
526 + }
527 + }
528 +
529 + // Set or update the transient with the new last_sent, timestamp, and stuck_count.
530 + // Reset the timestamp when not stuck or after an adjustment, so each new chunk size
531 + // gets a 10-minute window to prove itself before halving further.
532 + $previous_chunk_size = $stuck_data['adjusted_chunk_size'] ?? null;
533 + $reset_timestamp = ! $is_stuck || $adjusted_chunk_size !== $previous_chunk_size;
534 + set_transient(
535 + $transient_key,
536 + array(
537 + 'last_sent' => $last_sent,
538 + 'timestamp' => $reset_timestamp ? time() : $stuck_data['timestamp'],
539 + 'stuck_count' => $stuck_count,
540 + 'adjusted_chunk_size' => $adjusted_chunk_size,
541 + ),
542 + HOUR_IN_SECONDS
543 + );
544 +
545 + return $adjusted_chunk_size;
546 + }
547 +
548 + /**
396 549 * Set the status of the full sync action based on the objects that were sent.
550 + * Used to update the status of the module after sending a chunk of objects.
551 + * Since Full Sync logic chunking relies on order of items being processed in descending order, we need to sort
552 + * due to some modules (e.g. WooCommerce) changing the order while getting the objects.
397 553 *
398 554 * @access protected
399 555 *
400 556 * @param array $status This module Full Sync status.
@@ -402,10 +558,12 @@
402 558 *
403 559 * @return array The updated status.
404 560 */
405 561 protected function set_send_full_sync_actions_status( $status, $objects ) {
406 - $status['last_sent'] = end( $objects );
407 - $status['sent'] += count( $objects );
562 +
563 + $object_ids = $objects['object_ids'] ?? $objects;
564 + $status['last_sent'] = end( $object_ids );
565 + $status['sent'] += count( $object_ids );
408 566 return $status;
409 567 }
410 568
411 569 /**
@@ -412,12 +570,13 @@
412 570 * Immediately sends a single item without firing or enqueuing it
413 571 *
414 572 * @param string $action_name The action.
415 573 * @param array $data The data associated with the action.
574 + * @param string $key The key to use for the action.
416 575 */
417 - public function send_action( $action_name, $data = null ) {
576 + public function send_action( $action_name, $data = null, $key = null ) {
418 577 $sender = Sender::get_instance();
419 - return $sender->send_action( $action_name, $data );
578 + return $sender->send_action( $action_name, $data, $key );
420 579 }
421 580
422 581 /**
423 582 * Retrieve chunk IDs with previous interval end.
@@ -570,16 +729,15 @@
570 729 *
571 730 * @return array|bool An array of min and max ids for each batch. FALSE if no table can be found.
572 731 */
573 732 public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) {
574 - global $wpdb;
575 733
576 - if ( ! $this->table_name() ) {
734 + if ( ! $this->table() ) {
577 735 return false;
578 736 }
579 737
580 738 $results = array();
581 - $table = $wpdb->{$this->table_name()};
739 + $table = $this->table();
582 740 $current_max = 0;
583 741 $current_min = 1;
584 742 $id_field = $this->id_field();
585 743 $replicastore = new Replicastore();
@@ -627,13 +785,13 @@
627 785 * @return int total
628 786 */
629 787 public function total( $config ) {
630 788 global $wpdb;
631 - $table = $wpdb->{$this->table_name()};
789 + $table = $this->table();
632 790 $where = $this->get_where_sql( $config );
633 791
634 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
635 - return $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where" );
792 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
793 + return (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE $where" );
636 794 }
637 795
638 796 /**
639 797 * Retrieve the WHERE SQL clause based on the module config.
@@ -644,6 +802,67 @@
644 802 * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
645 803 */
646 804 public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
647 805 return '1=1';
806 + }
807 +
808 + /**
809 + * Filters objects and metadata based on maximum size constraints.
810 + * It always allows the first object with its metadata, even if they exceed the limit.
811 + *
812 + * @access public
813 + *
814 + * @param string $type The type of objects to filter (e.g., 'post' or 'comment').
815 + * @param array $objects The array of objects to filter (e.g., posts or comments).
816 + * @param array $metadata The array of metadata to filter.
817 + * @param int $max_meta_size Maximum size for individual objects.
818 + * @param int $max_total_size Maximum combined size for objects and metadata.
819 + * @return array An array containing the filtered object IDs, filtered objects, and filtered metadata.
820 + */
821 + public function filter_objects_and_metadata_by_size( $type, $objects, $metadata, $max_meta_size, $max_total_size ) {
822 + $filtered_objects = array();
823 + $filtered_metadata = array();
824 + $filtered_object_ids = array();
825 + $current_size = 0;
826 +
827 + foreach ( $objects as $object ) {
828 + $object_size = strlen( (string) maybe_serialize( $object ) );
829 + $current_metadata = array();
830 + $metadata_size = 0;
831 + $id_field = $this->id_field();
832 + $object_id = (int) ( is_object( $object ) ? $object->{$id_field} : $object[ $id_field ] );
833 +
834 + foreach ( $metadata as $key => $metadata_item ) {
835 + if ( (int) $metadata_item->{$type . '_id'} === $object_id ) {
836 + $metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
837 + if ( $metadata_item_size >= $max_meta_size ) {
838 + $metadata_item->meta_value = ''; // Trim metadata if too large.
839 + $metadata_item_size = strlen( (string) maybe_serialize( $metadata_item ) );
840 + }
841 + $current_metadata[] = $metadata_item;
842 + $metadata_size += $metadata_item_size;
843 +
844 + if ( ! empty( $filtered_object_ids ) && ( $current_size + $object_size + $metadata_size ) > $max_total_size ) {
845 + break 2; // Exit both loops.
846 + }
847 + unset( $metadata[ $key ] );
848 + }
849 + }
850 +
851 + // Always allow the first object with metadata.
852 + if ( empty( $filtered_object_ids ) || ( $current_size + $object_size + $metadata_size ) <= $max_total_size ) {
853 + $filtered_object_ids[] = strval( is_object( $object ) ? $object->{$id_field} : $object[ $id_field ] );
854 + $filtered_objects[] = $object;
855 + $filtered_metadata = array_merge( $filtered_metadata, $current_metadata );
856 + $current_size += $object_size + $metadata_size;
857 + } else {
858 + break;
859 + }
860 + }
861 +
862 + return array(
863 + $filtered_object_ids,
864 + $filtered_objects,
865 + $filtered_metadata,
866 + );
648 867 }
649 868 }