Completed
Push — update/sync-package ( 660c46 )
by
unknown
08:05
created

legacy/class.jetpack-sync-module-full-sync.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * This class does a full resync of the database by
5
 * enqueuing an outbound action for every single object
6
 * that we care about.
7
 *
8
 * This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained:
9
 * - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database
10
 * - for each object type, we page through the object IDs and enqueue them by firing some monitored actions
11
 * - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
12
 * - we fire a trigger for the entire array which the Jetpack_Sync_Listener then serializes and queues.
13
 */
14
15
class Jetpack_Sync_Module_Full_Sync extends Jetpack_Sync_Module {
16
	const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
17
	const FULL_SYNC_TIMEOUT    = 3600;
18
19
	public function name() {
20
		return 'full-sync';
21
	}
22
23
	function init_full_sync_listeners( $callable ) {
24
		// synthetic actions for full sync
25
		add_action( 'jetpack_full_sync_start', $callable, 10, 3 );
26
		add_action( 'jetpack_full_sync_end', $callable, 10, 2 );
27
		add_action( 'jetpack_full_sync_cancelled', $callable );
28
	}
29
30
	function init_before_send() {
31
		// this is triggered after actions have been processed on the server
32
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
33
	}
34
35
	function start( $module_configs = null ) {
36
		$was_already_running = $this->is_started() && ! $this->is_finished();
37
38
		// remove all evidence of previous full sync items and status
39
		$this->reset_data();
40
41
		if ( $was_already_running ) {
42
			/**
43
			 * Fires when a full sync is cancelled.
44
			 *
45
			 * @since 4.2.0
46
			 */
47
			do_action( 'jetpack_full_sync_cancelled' );
48
		}
49
50
		$this->update_status_option( 'started', time() );
51
		$this->update_status_option( 'params', $module_configs );
52
53
		$enqueue_status   = array();
54
		$full_sync_config = array();
55
		$include_empty    = false;
56
		$empty            = array();
57
		// default value is full sync
58
		if ( ! is_array( $module_configs ) ) {
59
			$module_configs = array();
60
			$include_empty  = true;
61
			foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
62
				$module_configs[ $module->name() ] = true;
63
			}
64
		}
65
66
		// set default configuration, calculate totals, and save configuration if totals > 0
67
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
68
			$module_name   = $module->name();
69
			$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
70
71
			if ( ! $module_config ) {
72
				continue;
73
			}
74
75
			if ( 'users' === $module_name && 'initial' === $module_config ) {
76
				$module_config = $module->get_initial_sync_user_config();
77
			}
78
79
			$enqueue_status[ $module_name ] = false;
80
81
			$total_items = $module->estimate_full_sync_actions( $module_config );
82
83
			// if there's information to process, configure this module
84
			if ( ! is_null( $total_items ) && $total_items > 0 ) {
85
				$full_sync_config[ $module_name ] = $module_config;
86
				$enqueue_status[ $module_name ]   = array(
87
					$total_items,   // total
88
					0,              // queued
89
					false,          // current state
90
				);
91
			} elseif ( $include_empty && $total_items === 0 ) {
92
				$empty[ $module_name ] = true;
93
			}
94
		}
95
96
		$this->set_config( $full_sync_config );
97
		$this->set_enqueue_status( $enqueue_status );
98
99
		$range = $this->get_content_range( $full_sync_config );
100
		/**
101
		 * Fires when a full sync begins. This action is serialized
102
		 * and sent to the server so that it knows a full sync is coming.
103
		 *
104
		 * @since 4.2.0
105
		 * @since 7.3.0 Added $range arg.
106
		 * @since 7.4.0 Added $empty arg.
107
		 *
108
		 * @param array $full_sync_config Sync configuration for all sync modules.
109
		 * @param array $range            Range of the sync items, containing min and max IDs for some item types.
110
		 * @param array $empty            The modules with no items to sync during a full sync.
111
		 */
112
		do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty );
113
114
		$this->continue_enqueuing( $full_sync_config, $enqueue_status );
115
116
		return true;
117
	}
118
119
	function continue_enqueuing( $configs = null, $enqueue_status = null ) {
120
		if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) {
121
			return;
122
		}
123
124
		// if full sync queue is full, don't enqueue more items
125
		$max_queue_size_full_sync = Jetpack_Sync_Settings::get_setting( 'max_queue_size_full_sync' );
126
		$full_sync_queue          = new Jetpack_Sync_Queue( 'full_sync' );
127
128
		$available_queue_slots = $max_queue_size_full_sync - $full_sync_queue->size();
129
130
		if ( $available_queue_slots <= 0 ) {
131
			return;
132
		} else {
133
			$remaining_items_to_enqueue = min( Jetpack_Sync_Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots );
134
		}
135
136
		if ( ! $configs ) {
137
			$configs = $this->get_config();
138
		}
139
140
		if ( ! $enqueue_status ) {
141
			$enqueue_status = $this->get_enqueue_status();
142
		}
143
144
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
145
			$module_name = $module->name();
146
147
			// skip module if not configured for this sync or module is done
148
			if ( ! isset( $configs[ $module_name ] )
149
				|| // no module config
150
					! $configs[ $module_name ]
151
				|| // no enqueue status
152
					! $enqueue_status[ $module_name ]
153
				|| // finished enqueuing this module
154
					true === $enqueue_status[ $module_name ][2] ) {
155
				continue;
156
			}
157
158
			list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][2] );
159
160
			$enqueue_status[ $module_name ][2] = $next_enqueue_state;
161
162
			// if items were processed, subtract them from the limit
163
			if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
164
				$enqueue_status[ $module_name ][1] += $items_enqueued;
165
				$remaining_items_to_enqueue        -= $items_enqueued;
166
			}
167
168
			// stop processing if we've reached our limit of items to enqueue
169
			if ( 0 >= $remaining_items_to_enqueue ) {
170
				$this->set_enqueue_status( $enqueue_status );
171
				return;
172
			}
173
		}
174
175
		$this->set_enqueue_status( $enqueue_status );
176
177
		// setting autoload to true means that it's faster to check whether we should continue enqueuing
178
		$this->update_status_option( 'queue_finished', time(), true );
179
180
		$range = $this->get_content_range( $configs );
181
182
		/**
183
		 * Fires when a full sync ends. This action is serialized
184
		 * and sent to the server.
185
		 *
186
		 * @since 4.2.0
187
		 * @since 7.3.0 Added $range arg.
188
		 *
189
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
190
		 * @param array  $range    Range of the sync items, containing min and max IDs for some item types.
191
		 */
192
		do_action( 'jetpack_full_sync_end', '', $range );
193
	}
194
195
	function get_range( $type ) {
196
		global $wpdb;
197
		if ( ! in_array( $type, array( 'comments', 'posts' ) ) ) {
198
			return array();
199
		}
200
201
		switch ( $type ) {
202
			case 'posts':
203
				$table     = $wpdb->posts;
204
				$id        = 'ID';
205
				$where_sql = Jetpack_Sync_Settings::get_blacklisted_post_types_sql();
206
207
				break;
208
			case 'comments':
209
				$table     = $wpdb->comments;
210
				$id        = 'comment_ID';
211
				$where_sql = Jetpack_Sync_Settings::get_comments_filter_sql();
212
				break;
213
		}
214
		$results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
215
		if ( isset( $results[0] ) ) {
216
			return $results[0];
217
		}
218
219
		return array();
220
	}
221
222
	private function get_content_range( $config ) {
223
		$range = array();
224
		// Only when we are sending the whole range do we want to send also the range
225 View Code Duplication
		if ( isset( $config['posts'] ) && $config['posts'] === true ) {
226
			$range['posts'] = $this->get_range( 'posts' );
227
		}
228
229 View Code Duplication
		if ( isset( $config['comments'] ) && $config['comments'] === true ) {
230
			$range['comments'] = $this->get_range( 'comments' );
231
		}
232
		return $range;
233
	}
234
235
	function update_sent_progress_action( $actions ) {
236
		// quick way to map to first items with an array of arrays
237
		$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
238
239
		// Total item counts for each action.
240
		$actions_with_total_counts = $this->get_actions_totals( $actions );
241
242
		if ( ! $this->is_started() || $this->is_finished() ) {
243
			return;
244
		}
245
246
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
247
			$this->update_status_option( 'send_started', time() );
248
		}
249
250
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
251
			$module_actions     = $module->get_full_sync_actions();
252
			$status_option_name = "{$module->name()}_sent";
253
			$total_option_name  = "{$status_option_name}_total";
254
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
255
			$items_sent_total   = $this->get_status_option( $total_option_name, 0 );
256
257
			foreach ( $module_actions as $module_action ) {
258
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
259
					$items_sent += $actions_with_counts[ $module_action ];
260
				}
261
262
				if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
263
					$items_sent_total += $actions_with_total_counts[ $module_action ];
264
				}
265
			}
266
267
			if ( $items_sent > 0 ) {
268
				$this->update_status_option( $status_option_name, $items_sent );
269
			}
270
271
			if ( 0 !== $items_sent_total ) {
272
				$this->update_status_option( $total_option_name, $items_sent_total );
273
			}
274
		}
275
276
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
277
			$this->update_status_option( 'finished', time() );
278
		}
279
	}
280
281
	public function get_action_name( $queue_item ) {
282
		if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
283
			return $queue_item[0];
284
		}
285
		return false;
286
	}
287
288
	/**
289
	 * Retrieve the total number of items we're syncing in a particular queue item (action).
290
	 * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
291
	 * represents the first (and only) chunk of items to sync in that action.
292
	 *
293
	 * @param array $queue_item Item of the sync queue that corresponds to a particular action.
294
	 * @return int Total number of items in the action.
295
	 */
296
	public function get_action_totals( $queue_item ) {
297
		if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
298
			if ( is_array( $queue_item[1][0] ) ) {
299
				// Let's count the items we sync in this action.
300
				return count( $queue_item[1][0] );
301
			}
302
			// -1 indicates that this action syncs all items by design.
303
			return -1;
304
		}
305
		return 0;
306
	}
307
308
	/**
309
	 * Retrieve the total number of items for a set of actions, grouped by action name.
310
	 *
311
	 * @param array $actions An array of actions.
312
	 * @return array An array, representing the total number of items, grouped per action.
313
	 */
314
	public function get_actions_totals( $actions ) {
315
		$totals = array();
316
317
		foreach ( $actions as $action ) {
318
			$name          = $this->get_action_name( $action );
319
			$action_totals = $this->get_action_totals( $action );
320
			if ( ! isset( $totals[ $name ] ) ) {
321
				$totals[ $name ] = 0;
322
			}
323
			$totals[ $name ] += $action_totals;
324
		}
325
326
		return $totals;
327
	}
328
329
	public function is_started() {
330
		return ! ! $this->get_status_option( 'started' );
331
	}
332
333
	public function is_finished() {
334
		return ! ! $this->get_status_option( 'finished' );
335
	}
336
337
	public function get_status() {
338
		$status = array(
339
			'started'        => $this->get_status_option( 'started' ),
340
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
341
			'send_started'   => $this->get_status_option( 'send_started' ),
342
			'finished'       => $this->get_status_option( 'finished' ),
343
			'sent'           => array(),
344
			'sent_total'     => array(),
345
			'queue'          => array(),
346
			'config'         => $this->get_status_option( 'params' ),
347
			'total'          => array(),
348
		);
349
350
		$enqueue_status = $this->get_enqueue_status();
351
352
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
353
			$name = $module->name();
354
355
			if ( ! isset( $enqueue_status[ $name ] ) ) {
356
				continue;
357
			}
358
359
			list( $total, $queued, $state ) = $enqueue_status[ $name ];
360
361
			if ( $total ) {
362
				$status['total'][ $name ] = $total;
363
			}
364
365
			if ( $queued ) {
366
				$status['queue'][ $name ] = $queued;
367
			}
368
369
			if ( $sent = $this->get_status_option( "{$name}_sent" ) ) {
370
				$status['sent'][ $name ] = $sent;
371
			}
372
373
			$sent_total = $this->get_status_option( "{$name}_sent_total" );
374
			if ( $sent_total ) {
375
				$status['sent_total'][ $name ] = $sent_total;
376
			}
377
		}
378
379
		return $status;
380
	}
381
382
	public function clear_status() {
383
		$prefix = self::STATUS_OPTION_PREFIX;
384
		Jetpack_Options::delete_raw_option( "{$prefix}_started" );
385
		Jetpack_Options::delete_raw_option( "{$prefix}_params" );
386
		Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
387
		Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
388
		Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
389
390
		$this->delete_enqueue_status();
391
392
		foreach ( Jetpack_Sync_Modules::get_modules() as $module ) {
393
			Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
394
			Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
395
		}
396
	}
397
398
	public function reset_data() {
399
		$this->clear_status();
400
		$this->delete_config();
401
402
		$listener = Jetpack_Sync_Listener::get_instance();
0 ignored issues
show
The method get_instance() does not seem to exist on object<Jetpack_Sync_Listener>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
403
		$listener->get_full_sync_queue()->reset();
404
	}
405
406
	private function get_status_option( $name, $default = null ) {
407
		$value = Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
408
409
		return is_numeric( $value ) ? intval( $value ) : $value;
410
	}
411
412
	private function update_status_option( $name, $value, $autoload = false ) {
413
		Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
414
	}
415
416
	private function set_enqueue_status( $new_status ) {
417
		Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
418
	}
419
420
	private function delete_enqueue_status() {
421
		return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
422
	}
423
424
	private function get_enqueue_status() {
425
		return Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
426
	}
427
428
	private function set_config( $config ) {
429
		Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
430
	}
431
432
	private function delete_config() {
433
		return Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
434
	}
435
436
	private function get_config() {
437
		return Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
438
	}
439
440
	private function write_option( $name, $value ) {
441
		// we write our own option updating code to bypass filters/caching/etc on set_option/get_option
442
		global $wpdb;
443
		$serialized_value = maybe_serialize( $value );
444
		// try updating, if no update then insert
445
		// TODO: try to deal with the fact that unchanged values can return updated_num = 0
446
		// below we used "insert ignore" to at least suppress the resulting error
447
		$updated_num = $wpdb->query(
448
			$wpdb->prepare(
449
				"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
450
				$serialized_value,
451
				$name
452
			)
453
		);
454
455
		if ( ! $updated_num ) {
456
			$updated_num = $wpdb->query(
457
				$wpdb->prepare(
458
					"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
459
					$name,
460
					$serialized_value
461
				)
462
			);
463
		}
464
		return $updated_num;
465
	}
466
467
	private function read_option( $name, $default = null ) {
468
		global $wpdb;
469
		$value = $wpdb->get_var(
470
			$wpdb->prepare(
471
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
472
				$name
473
			)
474
		);
475
		$value = maybe_unserialize( $value );
476
477
		if ( $value === null && $default !== null ) {
478
			return $default;
479
		}
480
481
		return $value;
482
	}
483
}
484