Completed
Push — update/sync-status-endpoint ( cb866f...e0f670 )
by
unknown
182:12 queued 173:27
created

Jetpack_Sync_Full::update_queue_progress()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 2
b 0
f 0
nc 2
nop 2
dl 0
loc 10
rs 9.4285
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 contains 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 obtain a full list of object IDs to sync via a single API call (hoping that since they're ints, they can all fit in RAM)
11
 * - we load the full objects for those IDs in chunks of Jetpack_Sync_Full::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
12
 * - we fire a trigger for the entire array which the Jetpack_Sync_Client then serializes and queues.
13
 */
14
15
require_once 'class.jetpack-sync-wp-replicastore.php';
16
17
class Jetpack_Sync_Full {
18
	const ARRAY_CHUNK_SIZE = 10;
19
	static $status_option = 'jetpack_full_sync_status';
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $status_option.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
20
	static $transient_timeout = 3600; // an hour
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $transient_timeout.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
21
	static $modules = array(
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $modules.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
22
		'wp_version',
23
		'constants',
24
		'functions',
25
		'options',
26
		'posts',
27
		'comments',
28
		'themes',
29
		'updates',
30
		'users',
31
		'terms',
32
		'network_options',
33
	);
34
35
	// singleton functions
36
	private static $instance;
37
	private $client;
38
39
	public static function getInstance() {
40
		if ( null === self::$instance ) {
41
			self::$instance = new self();
42
		}
43
44
		return self::$instance;
45
	}
46
47
	protected function __construct() {
48
		$this->init();
49
	}
50
51
	function init() {
52
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_posts', array( $this, 'expand_post_ids' ) );
53
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_comments', array( $this, 'expand_comment_ids' ) );
54
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_options', array( $this, 'expand_options' ) );
55
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_constants', array( $this, 'expand_constants' ) );
56
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
57
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_users', array( $this, 'expand_users' ) );
58
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) );
59
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_network_options', array(
60
			$this,
61
			'expand_network_options'
62
		) );
63
64
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_terms', array( $this, 'expand_term_ids' ) );
65
66
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
67
	}
68
69
	function start() {
70
		if( ! $this->should_start_full_sync() ) {
71
			return;
72
		}
73
		/**
74
		 * Fires when a full sync begins. This action is serialized
75
		 * and sent to the server so that it can clear the replica storage,
76
		 * and/or reset other data.
77
		 *
78
		 * @since 4.1
79
		 */
80
		do_action( 'jetpack_full_sync_start' );
81
		$this->set_status_queuing_started();
82
83
		$this->enqueue_all_constants();
84
		$this->enqueue_all_functions();
85
		$this->enqueue_all_options();
86
87
		if ( is_multisite() ) {
88
			$this->enqueue_all_network_options();
89
		}
90
91
		$this->enqueue_all_terms();
92
		$this->enqueue_all_theme_info();
93
		$this->enqueue_all_users();
94
		$this->enqueue_all_posts();
95
		$this->enqueue_all_comments();
96
		$this->enqueue_all_updates();
97
98
		$this->set_status_queuing_finished();
99
100
		$store = new Jetpack_Sync_WP_Replicastore();
101
		do_action( 'jetpack_full_sync_end', $store->checksum_all() );
102
	}
103
104
	private function should_start_full_sync() {
105
		$status = $this->get_status();
106
		// We should try sync if we haven't started it yet or if we have finished it.
107
		if( is_null( $status['started'] ) || is_integer( $status['finished'] ) ) {
108
			return true;
109
		}
110
		return false;
111
	}
112
113
	private function get_client() {
114
		if ( ! $this->client ) {
115
			$this->client = Jetpack_Sync_Client::getInstance();
116
		}
117
118
		return $this->client;
119
	}
120
121
	private function enqueue_all_constants() {
122
		$total = $this->get_client()->full_sync_constants();
123
		$this->update_queue_progress( 'constants', $total );
124
	}
125
126
	private function enqueue_all_functions() {
127
		$total = $this->get_client()->full_sync_callables();
128
		$this->update_queue_progress( 'functions', $total );
129
	}
130
131
	private function enqueue_all_options() {
132
		$total = $this->get_client()->force_sync_options();
133
		$this->update_queue_progress( 'options', $total );
134
	}
135
136
	private function enqueue_all_network_options() {
137
		$total = $this->get_client()->force_sync_network_options();
138
		$this->update_queue_progress( 'network_options', $total );
139
	}
140
141
	private function enqueue_all_terms() {
142
		global $wpdb;
143
144
		$taxonomies = get_taxonomies();
145
		$total_chunks_counter = 0;
146
		foreach ( $taxonomies as $taxonomy ) {
147
			// I hope this is never bigger than RAM...
148
			$term_ids = $wpdb->get_col( $wpdb->prepare( "SELECT term_id FROM $wpdb->term_taxonomy WHERE taxonomy = %s", $taxonomy ) ); // Should we set a limit here?
149
			// Request posts in groups of N for efficiency
150
			$chunked_term_ids = array_chunk( $term_ids, self::ARRAY_CHUNK_SIZE );
151
152
			// Send each chunk as an array of objects
153
			foreach ( $chunked_term_ids as $chunk ) {
154
				do_action( 'jetpack_full_sync_terms', $chunk, $taxonomy );
155
				$total_chunks_counter++;
156
			}
157
		}
158
159
		$this->update_queue_progress( 'terms', $total_chunks_counter );
160
161
	}
162
163
	private function enqueue_all_posts() {
164
		global $wpdb;
165
166
		$post_type_sql = Jetpack_Sync_Defaults::get_blacklisted_post_types_sql();
167
		$total = $this->enqueue_all_ids_as_action( 'jetpack_full_sync_posts', $wpdb->posts, 'ID', $post_type_sql );
168
		$this->update_queue_progress( 'posts', $total );
169
170
	}
171
172
	private function enqueue_all_ids_as_action( $action_name, $table_name, $id_field, $where_sql ) {
173
		global $wpdb;
174
175
		if ( ! $where_sql ) {
176
			$where_sql = "1 = 1";
177
		}
178
179
		$items_per_page = 500;
180
		$page = 1;
181
		$offset = ( $page * $items_per_page ) - $items_per_page;
182
		$chunk_count = 0;
183
		while( $ids = $wpdb->get_col( "SELECT {$id_field} FROM {$table_name} WHERE {$where_sql} ORDER BY {$id_field} asc LIMIT {$offset}, {$items_per_page}" ) ) {
184
			// Request posts in groups of N for efficiency
185
			$chunked_ids = array_chunk( $ids, self::ARRAY_CHUNK_SIZE );
186
187
			// Send each chunk as an array of objects
188
			foreach ( $chunked_ids as $chunk ) {
189
				/**
190
			 	 * Fires with a chunk of object IDs during full sync.
191
			 	 * These are expanded to full objects before upload
192
			 	 *
193
			 	 * @since 4.1
194
			 	 */
195
				do_action( $action_name, $chunk );
196
				$chunk_count++;
197
			}
198
199
			$page += 1;
200
			$offset = ( $page * $items_per_page ) - $items_per_page;
201
		}
202
		return $chunk_count;
203
	}
204
205
	public function expand_post_ids( $args ) {
206
		$post_ids = $args[0];
207
208
		$posts = array_map( array( 'WP_Post', 'get_instance' ), $post_ids );
209
		$posts = array_map( array( $this->get_client(), 'filter_post_content_and_add_links' ), $posts );
210
211
		return array(
212
			$posts,
213
			$this->get_metadata( $post_ids, 'post' ),
214
			$this->get_term_relationships( $post_ids )
215
		);
216
	}
217
218
	private function enqueue_all_comments() {
219
		global $wpdb;
220
221
		$total = $this->enqueue_all_ids_as_action( 'jetpack_full_sync_comments', $wpdb->comments, 'comment_ID', null );
222
		$this->update_queue_progress( 'comments', $total );
223
	}
224
225
	public function expand_comment_ids( $args ) {
226
		$comment_ids = $args[0];
227
		$comments    = get_comments( array(
228
			'include_unapproved' => true,
229
			'comment__in'        => $comment_ids,
230
		) );
231
232
		return array(
233
			$comments,
234
			$this->get_metadata( $comment_ids, 'comment' ),
235
		);
236
	}
237
238
	public function expand_term_ids( $args ) {
239
		global $wp_version;
240
		$term_ids = $args[0];
241
		$taxonomy = $args[1];
242
		// version 4.5 or higher
243
		if ( version_compare( $wp_version, 4.5, '>=' ) ) {
244
			$terms = get_terms( array(
245
				'taxonomy'   => $taxonomy,
246
				'hide_empty' => false,
247
				'include'    => $term_ids
248
			) );
249
		} else {
250
			$terms = get_terms( $taxonomy, array(
251
				'hide_empty' => false,
252
				'include'    => $term_ids
253
			) );
254
		}
255
256
		return $terms;
257
	}
258
259
	public function expand_options( $args ) {
260
		if ( $args[0] ) {
261
			return $this->get_client()->get_all_options();
262
		}
263
264
		return $args;
265
	}
266
267
	public function expand_constants( $args ) {
268
		if ( $args[0] ) {
269
			return $this->get_client()->get_all_constants();
270
		}
271
		return $args;
272
	}
273
274
	public function expand_callables( $args ) {
275
		if ( $args[0] ) {
276
			return $this->get_client()->get_all_callables();
277
		}
278
		return $args;
279
	}
280
281
	private function enqueue_all_users() {
282
		global $wpdb;
283
		$total = $this->enqueue_all_ids_as_action( 'jetpack_full_sync_users', $wpdb->users, 'ID', null );
284
		$this->update_queue_progress( 'users', $total );
285
	}
286
287
	public function expand_users( $args ) {
288
		$user_ids = $args[0];
289
		return array_map( array( $this->get_client(), 'sanitize_user' ), get_users( array( 'include' => $user_ids ) ) );
290
	}
291
292
	public function expand_network_options( $args ) {
293
		if ( $args[0] ) {
294
			return $this->get_client()->get_all_network_options();
295
		}
296
297
		return $args;
298
	}
299
300
	private function get_metadata( $ids, $meta_type ) {
301
		global $wpdb;
302
		$table = _get_meta_table( $meta_type );
303
		$id    = $meta_type . '_id';
304
		if ( ! $table ) {
305
			return array();
306
		}
307
308
		return $wpdb->get_results( "SELECT $id, meta_key, meta_value, meta_id FROM $table WHERE $id IN ( " . implode( ',', wp_parse_id_list( $ids ) ) . " )", OBJECT );
309
	}
310
311
	private function get_term_relationships( $ids ) {
312
		global $wpdb;
313
314
		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 );
315
	}
316
317
	// TODO:
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
318
	private function enqueue_all_theme_info() {
319
		$total = $this->get_client()->send_theme_info();
320
		$this->update_queue_progress( 'themes', $total );
321
	}
322
323
	private function enqueue_all_updates() {
324
325
		// check for updates
326
		$total = $this->get_client()->full_sync_updates();
327
		$this->update_queue_progress( 'updates', $total );
328
	}
329
330
	public function expand_updates( $args ) {
331
		if ( $args[0] ) {
332
			return $this->get_client()->get_all_updates();
333
		}
334
335
		return $args;
336
	}
337
	
338
	function update_sent_progress_action( $actions_sent ) {
339
		$modules_count = array();
340
		$status = $this->get_status();
341
		if ( is_null( $status['started'] ) || $status['finished'] ) {
342
			return;
343
		}
344
345
		if ( in_array( 'jetpack_full_sync_start', $actions_sent ) ) {
346
			$this->set_status_sending_started();
347
			$status['sent_started'] = time();
348
		}
349
350
		foreach( $actions_sent as $action ) {
351
			$module_key = $this->action_to_modules( $action );
352
			if ( $module_key ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $module_key of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
353
				$modules_count[ $module_key ] = isset( $modules_count[ $module_key ] ) ?  $modules_count[ $module_key ] + 1 : 1;
354
			}
355
356
		}
357
		foreach( $modules_count as $module => $count ) {
358
			$status[ 'sent' ][ $module ] = $this->update_sent_progress( $module, $count );
359
		}
360
361
		if ( in_array( 'jetpack_full_sync_end', $actions_sent ) ) {
362
			$this->set_status_sending_finished();
363
			$status['finished'] = time();
364
		}
365
		
366
		$this->update_status( $status );
367
	}
368
369
	function action_to_modules( $action ) {
370
		switch( $action ) {
371
			case 'jetpack_full_sync_constants':
372
				return 'constants';
373
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
374
375
			case 'jetpack_full_sync_callables':
376
				return 'functions';
377
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
378
379
			case 'jetpack_full_sync_options':
380
				return 'options';
381
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
382
383
			case 'jetpack_full_sync_network_options':
384
				return 'network_options';
385
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
386
387
			case 'jetpack_full_sync_terms':
388
				return 'terms';
389
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
390
391
			case 'jetpack_sync_current_theme_support':
392
				return 'themes';
393
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
394
395
			case 'jetpack_full_sync_users':
396
				return 'users';
397
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
398
399
			case 'jetpack_full_sync_posts':
400
				return 'posts';
401
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
402
403
			case 'jetpack_full_sync_comments':
404
				return 'comments';
405
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
406
407
			case 'jetpack_full_sync_updates':
408
				return 'updates';
409
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
410
411
		}
412
		return null;
413
	}
414
415
	private function set_status_queuing_started() {
416
		$status = $this->initial_status;
417
		$status[ 'started' ] = time();
418
		$this->update_status( $status );
419
	}
420
421
	private function set_status_queuing_finished() {
422
		$this->update_status( array( 'queue_finished' => time() ) );
423
	}
424
425
	// these are called by the Sync Client when it sees that the full sync start/end actions have actually been transmitted
426
	public function set_status_sending_started() {
427
		/**
428
		 * Fires when the full_sync_start action is actually transmitted.
429
		 * This is useful for telling the user the status of full synchronization.
430
		 *
431
		 * @since 4.1
432
		 */
433
434
		do_action( 'jetpack_full_sync_start_sent' );
435
436
	}
437
438
	public function set_status_sending_finished() {
439
		/**
440
		 * Fires when the full_sync_end action is actually transmitted.
441
		 * This is useful for telling the user the status of full synchronization.
442
		 *
443
		 * @since 4.1
444
		 */
445
		do_action( 'jetpack_full_sync_end_sent' );
446
	}
447
	
448
	private $initial_status = array(
449
		'started' => null,
450
		'queue_finished' => null,
451
		'sent_started' => null,
452
		'finished' => null,
453
		'sent' => array(),
454
		'queue' => array(),
455
	);
456
457
	public function get_status() {
458
		return get_option( self::$status_option, $this->initial_status );
459
	}
460
461
462
	public function update_status( $status ) {
463
		return update_option(
464
			self::$status_option,
465
			array_merge( $this->get_status(), $status )
466
		);
467
	}
468
469
	private function clear_status() {
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
470
		delete_option( self::$status_option );
471
	}
472
473
	public function update_queue_progress( $module, $data ) {
474
		$status = $this->get_status();
475
		if ( isset( $status['queue'][ $module ] ) )  {
476
			$status['queue'][ $module ] = $data + $status['queue'][ $module ];
477
		} else {
478
			$status['queue'][ $module ] = $data;
479
		}
480
481
		return $this->update_status( $status );
482
	}
483
484
	public function update_sent_progress( $module, $data ) {
485
		$status = $this->get_status();
486
		if ( isset( $status['sent'][ $module ] ) )  {
487
			return $data + $status['sent'][ $module ];
488
		} else {
489
			return $data;
490
		}
491
	}
492
	
493
}
494