PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules / class-module.php
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules Last commit date
class-attachments.php 9 months ago class-callables.php 1 month ago class-comments.php 2 months ago class-constants.php 9 months ago class-full-sync-immediately.php 3 months ago class-full-sync.php 2 months ago class-import.php 9 months ago class-menus.php 2 months ago class-meta.php 2 months ago class-module.php 5 months ago class-network-options.php 9 months ago class-options.php 2 months ago class-plugins.php 2 months ago class-posts.php 2 months ago class-protect.php 9 months ago class-search.php 2 months ago class-stats.php 6 months ago class-term-relationships.php 9 months ago class-terms.php 2 months ago class-themes.php 2 months ago class-updates.php 8 months ago class-users.php 1 month ago class-woocommerce-hpos-orders.php 1 month ago class-woocommerce-products.php 8 months ago class-woocommerce.php 1 month ago class-wp-super-cache.php 9 months ago
class-module.php
869 lines
1 <?php
2 /**
3 * A base abstraction of a sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8 namespace Automattic\Jetpack\Sync\Modules;
9
10 use Automattic\Jetpack\Sync\Defaults;
11 use Automattic\Jetpack\Sync\Functions;
12 use Automattic\Jetpack\Sync\Listener;
13 use Automattic\Jetpack\Sync\Replicastore;
14 use Automattic\Jetpack\Sync\Sender;
15 use Automattic\Jetpack\Sync\Settings;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit( 0 );
19 }
20
21 /**
22 * Basic methods implemented by Jetpack Sync extensions.
23 *
24 * @abstract
25 */
26 abstract class Module {
27 /**
28 * Number of items per chunk when grouping objects for performance reasons.
29 *
30 * @access public
31 *
32 * @var int
33 */
34 const ARRAY_CHUNK_SIZE = 10;
35
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 /**
66 * Sync module name.
67 *
68 * @access public
69 *
70 * @return string
71 */
72 abstract public function name();
73
74 /**
75 * The id field in the database.
76 *
77 * @access public
78 *
79 * @return string
80 */
81 public function id_field() {
82 return 'ID';
83 }
84
85 /**
86 * The table name.
87 *
88 * @access public
89 *
90 * @return string|bool
91 * @deprecated since 3.11.0 Use table() instead.
92 */
93 public function table_name() {
94 _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\Module->table' );
95 return false;
96 }
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
120 // phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
121
122 /**
123 * Retrieve a sync object by its ID.
124 *
125 * @access public
126 *
127 * @param string $object_type Type of the sync object.
128 * @param int $id ID of the sync object.
129 * @return mixed Object, or false if the object is invalid.
130 */
131 public function get_object_by_id( $object_type, $id ) {
132 return false;
133 }
134
135 /**
136 * Initialize callables action listeners.
137 * Override these to set up listeners and set/reset data/defaults.
138 *
139 * @access public
140 *
141 * @param callable $callable Action handler callable.
142 */
143 public function init_listeners( $callable ) {
144 }
145
146 /**
147 * Initialize module action listeners for full sync.
148 *
149 * @access public
150 *
151 * @param callable $callable Action handler callable.
152 */
153 public function init_full_sync_listeners( $callable ) {
154 }
155
156 /**
157 * Initialize the module in the sender.
158 *
159 * @access public
160 */
161 public function init_before_send() {
162 }
163
164 /**
165 * Set module defaults.
166 *
167 * @access public
168 */
169 public function set_defaults() {
170 }
171
172 /**
173 * Perform module cleanup.
174 * Usually triggered when uninstalling the plugin.
175 *
176 * @access public
177 */
178 public function reset_data() {
179 }
180
181 /**
182 * Enqueue the module actions for full sync.
183 *
184 * @access public
185 *
186 * @param array $config Full sync configuration for this sync module.
187 * @param int $max_items_to_enqueue Maximum number of items to enqueue.
188 * @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
189 * @return array Number of actions enqueued, and next module state.
190 */
191 public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
192 // In subclasses, return the number of actions enqueued, and next module state (true == done).
193 return array( null, true );
194 }
195
196 /**
197 * Retrieve an estimated number of actions that will be enqueued.
198 *
199 * @access public
200 *
201 * @param array $config Full sync configuration for this sync module.
202 * @return int Number of items yet to be enqueued.
203 */
204 public function estimate_full_sync_actions( $config ) {
205 // In subclasses, return the number of items yet to be enqueued.
206 return null;
207 }
208
209 // phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
210
211 /**
212 * Retrieve the actions that will be sent for this module during a full sync.
213 *
214 * @access public
215 *
216 * @return array Full sync actions of this module.
217 */
218 public function get_full_sync_actions() {
219 return array();
220 }
221
222 /**
223 * Get the number of actions that we care about.
224 *
225 * @access protected
226 *
227 * @param array $action_names Action names we're interested in.
228 * @param array $actions_to_count Unfiltered list of actions we want to count.
229 * @return array Number of actions that we're interested in.
230 */
231 protected function count_actions( $action_names, $actions_to_count ) {
232 return count( array_intersect( $action_names, $actions_to_count ) );
233 }
234
235 /**
236 * Calculate the checksum of one or more values.
237 *
238 * @access protected
239 *
240 * @param mixed $values Values to calculate checksum for.
241 * @param bool $sort If $values should have ksort called on it.
242 * @return int The checksum.
243 */
244 protected function get_check_sum( $values, $sort = true ) {
245 // Associative array order changes the generated checksum value.
246 if ( $sort && is_array( $values ) ) {
247 $this->recursive_ksort( $values );
248 }
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 );
255 }
256
257 /**
258 * Recursively call ksort on an Array
259 *
260 * @param array $values Array.
261 */
262 private function recursive_ksort( &$values ) {
263 ksort( $values );
264 foreach ( $values as &$value ) {
265 if ( is_array( $value ) ) {
266 $this->recursive_ksort( $value );
267 }
268 }
269 }
270
271 /**
272 * Whether a particular checksum in a set of checksums is valid.
273 *
274 * @access protected
275 *
276 * @param array $sums_to_check Array of checksums.
277 * @param string $name Name of the checksum.
278 * @param int $new_sum Checksum to compare against.
279 * @return boolean Whether the checksum is valid.
280 */
281 protected function still_valid_checksum( $sums_to_check, $name, $new_sum ) {
282 if ( isset( $sums_to_check[ $name ] ) && $sums_to_check[ $name ] === $new_sum ) {
283 return true;
284 }
285
286 return false;
287 }
288
289 /**
290 * Enqueue all items of a sync type as an action.
291 *
292 * @access protected
293 *
294 * @param string $action_name Name of the action.
295 * @param string $table_name Name of the database table.
296 * @param string $id_field Name of the ID field in the database.
297 * @param string $where_sql The SQL WHERE clause to filter to the desired items.
298 * @param int $max_items_to_enqueue Maximum number of items to enqueue in the same time.
299 * @param boolean $state Whether enqueueing has finished.
300 * @return array Array, containing the number of chunks and TRUE, indicating enqueueing has finished.
301 */
302 protected function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql, $max_items_to_enqueue, $state ) {
303 global $wpdb;
304
305 if ( ! $where_sql ) {
306 $where_sql = '1 = 1';
307 }
308
309 $items_per_page = 1000;
310 $page = 1;
311 $chunk_count = 0;
312 $previous_interval_end = $state ? $state : '~0';
313 $listener = Listener::get_instance();
314
315 // Count down from max_id to min_id so we get newest posts/comments/etc first.
316 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
317 while ( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} AND {$id_field} < {$previous_interval_end} ORDER BY {$id_field} DESC LIMIT {$items_per_page}" ) ) {
318 // Request posts in groups of N for efficiency.
319 $chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE );
320
321 // If we hit our row limit, process and return.
322 if ( $chunk_count + count( $chunked_ids ) >= $max_items_to_enqueue ) {
323 $remaining_items_count = $max_items_to_enqueue - $chunk_count;
324 $remaining_items = array_slice( $chunked_ids, 0, $remaining_items_count );
325 $remaining_items_with_previous_interval_end = $this->get_chunks_with_preceding_end( $remaining_items, $previous_interval_end );
326 $listener->bulk_enqueue_full_sync_actions( $action_name, $remaining_items_with_previous_interval_end );
327
328 $last_chunk = end( $remaining_items );
329 return array( $remaining_items_count + $chunk_count, end( $last_chunk ) );
330 }
331 $chunked_ids_with_previous_end = $this->get_chunks_with_preceding_end( $chunked_ids, $previous_interval_end );
332
333 $listener->bulk_enqueue_full_sync_actions( $action_name, $chunked_ids_with_previous_end );
334
335 $chunk_count += count( $chunked_ids );
336 ++$page;
337 // The $ids are ordered in descending order.
338 $previous_interval_end = end( $ids );
339 }
340
341 if ( $wpdb->last_error ) {
342 // return the values that were passed in so all these chunks get retried.
343 return array( $max_items_to_enqueue, $state );
344 }
345
346 return array( $chunk_count, true );
347 }
348
349 /**
350 * Given the Module Full Sync Configuration and Status return the next chunk of items to send.
351 *
352 * @param array $config This module Full Sync configuration.
353 * @param array $status This module Full Sync status.
354 * @param int $chunk_size Chunk size.
355 *
356 * @return array|object|null
357 */
358 public function get_next_chunk( $config, $status, $chunk_size ) {
359 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
360 global $wpdb;
361 return $wpdb->get_col(
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 "
370 );
371 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
372 }
373
374 /**
375 * Return last_item to send for Module Full Sync Configuration.
376 *
377 * @param array $config This module Full Sync configuration.
378 *
379 * @return array|object|null
380 */
381 public function get_last_item( $config ) {
382 global $wpdb;
383 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
384 return $wpdb->get_var(
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 "
392 );
393 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
394 }
395
396 /**
397 * Return the initial last sent object.
398 *
399 * @return string|array initial status.
400 */
401 public function get_initial_last_sent() {
402 return '~0';
403 }
404
405 /**
406 * Immediately send all items of a sync type as an action.
407 *
408 * @access protected
409 *
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.
414 *
415 * @return array Status, the module full sync status updated.
416 */
417 public function send_full_sync_actions( $config, $status, $send_until, $started ) {
418 global $wpdb;
419
420 if ( empty( $status['last_sent'] ) ) {
421 $status['last_sent'] = $this->get_initial_last_sent();
422 }
423
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 );
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
438 $chunks_sent = 0;
439
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'];
446
447 while ( $chunks_sent < $limits['max_chunks'] && microtime( true ) < $send_until ) {
448 $objects = $this->get_next_chunk( $config, $status, $limits['chunk_size'] );
449
450 if ( $wpdb->last_error ) {
451 $status['error'] = true;
452 return $status;
453 }
454
455 if ( empty( $objects ) ) {
456 $status['finished'] = true;
457 return $status;
458 }
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;
469 }
470
471 // Updated the sent and last_sent status.
472 $status = $this->set_send_full_sync_actions_status( $status, $objects );
473 if ( $last_item === $status['last_sent'] ) {
474 $status['finished'] = true;
475 return $status;
476 }
477 }
478
479 return $status;
480 }
481
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 /**
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.
553 *
554 * @access protected
555 *
556 * @param array $status This module Full Sync status.
557 * @param array $objects This module Full Sync objects.
558 *
559 * @return array The updated status.
560 */
561 protected function set_send_full_sync_actions_status( $status, $objects ) {
562
563 $object_ids = $objects['object_ids'] ?? $objects;
564 $status['last_sent'] = end( $object_ids );
565 $status['sent'] += count( $object_ids );
566 return $status;
567 }
568
569 /**
570 * Immediately sends a single item without firing or enqueuing it
571 *
572 * @param string $action_name The action.
573 * @param array $data The data associated with the action.
574 * @param string $key The key to use for the action.
575 */
576 public function send_action( $action_name, $data = null, $key = null ) {
577 $sender = Sender::get_instance();
578 return $sender->send_action( $action_name, $data, $key );
579 }
580
581 /**
582 * Retrieve chunk IDs with previous interval end.
583 *
584 * @access protected
585 *
586 * @param array $chunks All remaining items.
587 * @param int $previous_interval_end The last item from the previous interval.
588 * @return array Chunk IDs with the previous interval end.
589 */
590 protected function get_chunks_with_preceding_end( $chunks, $previous_interval_end ) {
591 $chunks_with_ends = array();
592 foreach ( $chunks as $chunk ) {
593 $chunks_with_ends[] = array(
594 'ids' => $chunk,
595 'previous_end' => $previous_interval_end,
596 );
597 // Chunks are ordered in descending order.
598 $previous_interval_end = end( $chunk );
599 }
600 return $chunks_with_ends;
601 }
602
603 /**
604 * Get metadata of a particular object type within the designated meta key whitelist.
605 *
606 * @access protected
607 *
608 * @todo Refactor to use $wpdb->prepare() on the SQL query.
609 *
610 * @param array $ids Object IDs.
611 * @param string $meta_type Meta type.
612 * @param array $meta_key_whitelist Meta key whitelist.
613 * @return array Unserialized meta values.
614 */
615 protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) {
616 global $wpdb;
617 $table = _get_meta_table( $meta_type );
618 $id = $meta_type . '_id';
619 if ( ! $table ) {
620 return array();
621 }
622
623 $private_meta_whitelist_sql = "'" . implode( "','", array_map( 'esc_sql', $meta_key_whitelist ) ) . "'";
624
625 return array_map(
626 array( $this, 'unserialize_meta' ),
627 $wpdb->get_results(
628 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
629 "SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )' .
630 " AND meta_key IN ( $private_meta_whitelist_sql ) ",
631 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
632 OBJECT
633 )
634 );
635 }
636
637 /**
638 * Initialize listeners for the particular meta type.
639 *
640 * @access public
641 *
642 * @param string $meta_type Meta type.
643 * @param callable $callable Action handler callable.
644 */
645 public function init_listeners_for_meta_type( $meta_type, $callable ) {
646 add_action( "added_{$meta_type}_meta", $callable, 10, 4 );
647 add_action( "updated_{$meta_type}_meta", $callable, 10, 4 );
648 add_action( "deleted_{$meta_type}_meta", $callable, 10, 4 );
649 }
650
651 /**
652 * Initialize meta whitelist handler for the particular meta type.
653 *
654 * @access public
655 *
656 * @param string $meta_type Meta type.
657 * @param callable $whitelist_handler Action handler callable.
658 */
659 public function init_meta_whitelist_handler( $meta_type, $whitelist_handler ) {
660 add_filter( "jetpack_sync_before_enqueue_added_{$meta_type}_meta", $whitelist_handler );
661 add_filter( "jetpack_sync_before_enqueue_updated_{$meta_type}_meta", $whitelist_handler );
662 add_filter( "jetpack_sync_before_enqueue_deleted_{$meta_type}_meta", $whitelist_handler );
663 }
664
665 /**
666 * Retrieve the term relationships for the specified object IDs.
667 *
668 * @access protected
669 *
670 * @todo This feels too specific to be in the abstract sync Module class. Move it?
671 *
672 * @param array $ids Object IDs.
673 * @return array Term relationships - object ID and term taxonomy ID pairs.
674 */
675 protected function get_term_relationships( $ids ) {
676 global $wpdb;
677
678 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
679 return $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM $wpdb->term_relationships WHERE object_id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . ' )', OBJECT );
680 }
681
682 /**
683 * Unserialize the value of a meta object, if necessary.
684 *
685 * @access public
686 *
687 * @param object $meta Meta object.
688 * @return object Meta object with possibly unserialized value.
689 */
690 public function unserialize_meta( $meta ) {
691 $meta->meta_value = maybe_unserialize( $meta->meta_value );
692 return $meta;
693 }
694
695 /**
696 * Retrieve a set of objects by their IDs.
697 *
698 * @access public
699 *
700 * @param string $object_type Object type.
701 * @param array $ids Object IDs.
702 * @return array Array of objects.
703 */
704 public function get_objects_by_id( $object_type, $ids ) {
705 if ( empty( $ids ) || empty( $object_type ) ) {
706 return array();
707 }
708
709 $objects = array();
710 foreach ( (array) $ids as $id ) {
711 $object = $this->get_object_by_id( $object_type, $id );
712
713 // Only add object if we have the object.
714 if ( $object ) {
715 $objects[ $id ] = $object;
716 }
717 }
718
719 return $objects;
720 }
721
722 /**
723 * Gets a list of minimum and maximum object ids for each batch based on the given batch size.
724 *
725 * @access public
726 *
727 * @param int $batch_size The batch size for objects.
728 * @param string|bool $where_sql The sql where clause minus 'WHERE', or false if no where clause is needed.
729 *
730 * @return array|bool An array of min and max ids for each batch. FALSE if no table can be found.
731 */
732 public function get_min_max_object_ids_for_batches( $batch_size, $where_sql = false ) {
733
734 if ( ! $this->table() ) {
735 return false;
736 }
737
738 $results = array();
739 $table = $this->table();
740 $current_max = 0;
741 $current_min = 1;
742 $id_field = $this->id_field();
743 $replicastore = new Replicastore();
744
745 $total = $replicastore->get_min_max_object_id(
746 $id_field,
747 $table,
748 $where_sql,
749 false
750 );
751
752 while ( $total->max > $current_max ) {
753 $where = $where_sql ?
754 $where_sql . " AND $id_field > $current_max" :
755 "$id_field > $current_max";
756 $result = $replicastore->get_min_max_object_id(
757 $id_field,
758 $table,
759 $where,
760 $batch_size
761 );
762 if ( empty( $result->min ) && empty( $result->max ) ) {
763 // Our query produced no min and max. We can assume the min from the previous query,
764 // and the total max we found in the initial query.
765 $current_max = (int) $total->max;
766 $result = (object) array(
767 'min' => $current_min,
768 'max' => $current_max,
769 );
770 } else {
771 $current_min = (int) $result->min;
772 $current_max = (int) $result->max;
773 }
774 $results[] = $result;
775 }
776
777 return $results;
778 }
779
780 /**
781 * Return Total number of objects.
782 *
783 * @param array $config Full Sync config.
784 *
785 * @return int total
786 */
787 public function total( $config ) {
788 global $wpdb;
789 $table = $this->table();
790 $where = $this->get_where_sql( $config );
791
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" );
794 }
795
796 /**
797 * Retrieve the WHERE SQL clause based on the module config.
798 *
799 * @access public
800 *
801 * @param array $config Full sync configuration for this sync module.
802 * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
803 */
804 public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
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 );
867 }
868 }
869