Completed
Push — fix/check-queue-status-before-... ( b1b857...f572c6 )
by
unknown
07:06
created

Full_Sync::_continue_enqueuing()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 8
nop 1
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
1
<?php
2
/**
3
 * Full sync module.
4
 *
5
 * @package automattic/jetpack-sync
6
 */
7
8
namespace Automattic\Jetpack\Sync\Modules;
9
10
use Automattic\Jetpack\Sync\Listener;
11
use Automattic\Jetpack\Sync\Modules;
12
use Automattic\Jetpack\Sync\Queue;
13
use Automattic\Jetpack\Sync\Settings;
14
15
/**
16
 * This class does a full resync of the database by
17
 * enqueuing an outbound action for every single object
18
 * that we care about.
19
 *
20
 * This class, and its related class Jetpack_Sync_Module, contain a few non-obvious optimisations that should be explained:
21
 * - we fire an action called jetpack_full_sync_start so that WPCOM can erase the contents of the cached database
22
 * - for each object type, we page through the object IDs and enqueue them by firing some monitored actions
23
 * - we load the full objects for those IDs in chunks of Jetpack_Sync_Module::ARRAY_CHUNK_SIZE (to reduce the number of MySQL calls)
24
 * - we fire a trigger for the entire array which the Automattic\Jetpack\Sync\Listener then serializes and queues.
25
 */
26
class Full_Sync extends Module {
27
	/**
28
	 * Prefix of the full sync status option name.
29
	 *
30
	 * @var string
31
	 */
32
	const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
33
34
	/**
35
	 * Timeout between the previous and the next allowed full sync.
36
	 *
37
	 * @todo Remove this as it's no longer used since https://github.com/Automattic/jetpack/pull/4561
38
	 *
39
	 * @var int
40
	 */
41
	const FULL_SYNC_TIMEOUT = 3600;
42
43
	/**
44
	 * Sync module name.
45
	 *
46
	 * @access public
47
	 *
48
	 * @return string
49
	 */
50
	public function name() {
51
		return 'full-sync';
52
	}
53
54
	/**
55
	 * Initialize action listeners for full sync.
56
	 *
57
	 * @access public
58
	 *
59
	 * @param callable $callable Action handler callable.
60
	 */
61
	public function init_full_sync_listeners( $callable ) {
62
		// Synthetic actions for full sync.
63
		add_action( 'jetpack_full_sync_start', $callable, 10, 3 );
64
		add_action( 'jetpack_full_sync_end', $callable, 10, 2 );
65
		add_action( 'jetpack_full_sync_cancelled', $callable );
66
	}
67
68
	/**
69
	 * Initialize the module in the sender.
70
	 *
71
	 * @access public
72
	 */
73
	public function init_before_send() {
74
		// This is triggered after actions have been processed on the server.
75
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
76
	}
77
78
	/**
79
	 * Start a full sync.
80
	 *
81
	 * @access public
82
	 *
83
	 * @param array $module_configs Full sync configuration for all sync modules.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $module_configs not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
84
	 * @return bool Always returns true at success.
85
	 */
86
	public function start( $module_configs = null ) {
87
		$was_already_running = $this->is_started() && ! $this->is_finished();
88
89
		// Remove all evidence of previous full sync items and status.
90
		$this->reset_data();
91
92
		if ( $was_already_running ) {
93
			/**
94
			 * Fires when a full sync is cancelled.
95
			 *
96
			 * @since 4.2.0
97
			 */
98
			do_action( 'jetpack_full_sync_cancelled' );
99
		}
100
101
		$this->update_status_option( 'started', time() );
102
		$this->update_status_option( 'params', $module_configs );
103
104
		$enqueue_status   = array();
105
		$full_sync_config = array();
106
		$include_empty    = false;
107
		$empty            = array();
108
109
		// Default value is full sync.
110
		if ( ! is_array( $module_configs ) ) {
111
			$module_configs = array();
112
			$include_empty  = true;
113
			foreach ( Modules::get_modules() as $module ) {
0 ignored issues
show
Bug introduced by
The expression \Automattic\Jetpack\Sync\Modules::get_modules() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
114
				$module_configs[ $module->name() ] = true;
115
			}
116
		}
117
118
		// Set default configuration, calculate totals, and save configuration if totals > 0.
119
		foreach ( Modules::get_modules() as $module ) {
0 ignored issues
show
Bug introduced by
The expression \Automattic\Jetpack\Sync\Modules::get_modules() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
120
			$module_name   = $module->name();
121
			$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
122
123
			if ( ! $module_config ) {
124
				continue;
125
			}
126
127
			if ( 'users' === $module_name && 'initial' === $module_config ) {
128
				$module_config = $module->get_initial_sync_user_config();
129
			}
130
131
			$enqueue_status[ $module_name ] = false;
132
133
			$total_items = $module->estimate_full_sync_actions( $module_config );
134
135
			// If there's information to process, configure this module.
136
			if ( ! is_null( $total_items ) && $total_items > 0 ) {
137
				$full_sync_config[ $module_name ] = $module_config;
138
				$enqueue_status[ $module_name ]   = array(
139
					$total_items,   // Total.
140
					0,              // Queued.
141
					false,          // Current state.
142
				);
143
			} elseif ( $include_empty && 0 === $total_items ) {
144
				$empty[ $module_name ] = true;
145
			}
146
		}
147
148
		$this->set_config( $full_sync_config );
149
		$this->set_enqueue_status( $enqueue_status );
150
151
		$range = $this->get_content_range( $full_sync_config );
152
		/**
153
		 * Fires when a full sync begins. This action is serialized
154
		 * and sent to the server so that it knows a full sync is coming.
155
		 *
156
		 * @since 4.2.0
157
		 * @since 7.3.0 Added $range arg.
158
		 * @since 7.4.0 Added $empty arg.
159
		 *
160
		 * @param array $full_sync_config Sync configuration for all sync modules.
161
		 * @param array $range            Range of the sync items, containing min and max IDs for some item types.
162
		 * @param array $empty            The modules with no items to sync during a full sync.
163
		 */
164
		do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty );
165
166
		$this->continue_enqueuing( $full_sync_config );
167
168
		return true;
169
	}
170
171
	/**
172
	 * Enqueue the next items to sync.
173
	 *
174
	 * @access public
175
	 *
176
	 * @param array $configs Full sync configuration for all sync modules.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $configs not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
177
	 */
178
	public function continue_enqueuing( $configs = null ) {
179
		if ( ! $this->is_started() ) {
180
			return;
181
		}
182
		if ( ! $this->attempt_enqueue_lock() ) {
183
			return;
184
		}
185
		if ( $this->get_status_option( 'queue_finished' ) ) {
186
			$this->remove_enqueue_lock();
187
			return;
188
		}
189
		$this->_continue_enqueuing( $configs );
190
191
		$this->remove_enqueue_lock();
192
	}
193
194
	/**
195
	 *
196
	 * Get Available Full Sync queue slots.
197
	 *
198
	 * @return int
199
	 */
200
	public function get_available_queue_slots() {
201
		// If full sync queue is full, don't enqueue more items.
202
		$max_queue_size_full_sync = Settings::get_setting( 'max_queue_size_full_sync' );
203
		$full_sync_queue          = new Queue( 'full_sync' );
204
205
		return $max_queue_size_full_sync - $full_sync_queue->size();
206
	}
207
208
	/**
209
	 * Get Modules that are configured to Full Sync and haven't finished enqueuing
210
	 *
211
	 * @param array $configs Full sync configuration for all sync modules.
212
	 * @param array $enqueue_status Current status of the queue, indexed by sync modules.
213
	 *
214
	 * @return array
215
	 */
216
	public function get_remaining_modules_to_enqueue( $configs, $enqueue_status ) {
217
		return array_filter(
218
			Modules::get_modules(),
219
			function ( $module ) use ( $configs, $enqueue_status ) {
220
				// Skip module if not configured for this sync or module is done.
221
				if ( ! isset( $configs[ $module->name() ] ) ) {
222
					return false;
223
				}
224
				if ( ! $configs[ $module->name() ] ) {
225
					return false;
226
				}
227
				if ( isset( $enqueue_status[ $module->name() ] ) ) {
228
					if ( true === $enqueue_status[ $module->name() ][2] ) {
229
						return false;
230
					}
231
				}
232
233
				return true;
234
			}
235
		);
236
	}
237
238
	/**
239
	 * Enqueue the next items to sync.
240
	 *
241
	 * @access public
242
	 *
243
	 * @param array $configs Full sync configuration for all sync modules.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $configs not be array|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
244
	 */
245
	private function _continue_enqueuing( $configs = null ) {
246
		if ( ! $configs ) {
247
			$configs = $this->get_config();
248
		}
249
250
		$remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $this->get_available_queue_slots() );
251
		$enqueue_status             = $this->get_enqueue_status();
252
253
		$next_enqueue_state = true;
254
		foreach ( $this->get_remaining_modules_to_enqueue( $configs, $enqueue_status ) as $module ) {
255
			if ( 0 >= $remaining_items_to_enqueue || true !== $next_enqueue_state ) {
256
				$this->set_enqueue_status( $enqueue_status );
257
				return;
258
			}
259
			list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module->name() ], $remaining_items_to_enqueue, $enqueue_status[ $module->name() ][2] );
260
261
			$enqueue_status[ $module->name() ][2] = $next_enqueue_state;
262
263
			// If items were processed, subtract them from the limit.
264
			if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
265
				$enqueue_status[ $module->name() ][1] += $items_enqueued;
266
				$remaining_items_to_enqueue           -= $items_enqueued;
267
			}
268
		}
269
270
		$this->queue_full_sync_end( $configs );
271
	}
272
273
	/**
274
	 * Enqueue 'jetpack_full_sync_end' and update 'queue_finished' status.
275
	 *
276
	 * @access public
277
	 *
278
	 * @param array $configs Full sync configuration for all sync modules.
279
	 */
280
	public function queue_full_sync_end( $configs ) {
281
		$range = $this->get_content_range( $configs );
282
283
		/**
284
		 * Fires when a full sync ends. This action is serialized
285
		 * and sent to the server.
286
		 *
287
		 * @since 4.2.0
288
		 * @since 7.3.0 Added $range arg.
289
		 *
290
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
291
		 * @param array  $range    Range of the sync items, containing min and max IDs for some item types.
292
		 */
293
		do_action( 'jetpack_full_sync_end', '', $range );
294
295
		// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
296
		$this->update_status_option( 'queue_finished', time(), true );
297
	}
298
299
	/**
300
	 * Get the range (min ID, max ID and total items) of items to sync.
301
	 *
302
	 * @access public
303
	 *
304
	 * @param string $type Type of sync item to get the range for.
305
	 * @return array Array of min ID, max ID and total items in the range.
306
	 */
307
	public function get_range( $type ) {
308
		global $wpdb;
309
		if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
310
			return array();
311
		}
312
313
		switch ( $type ) {
314
			case 'posts':
315
				$table     = $wpdb->posts;
316
				$id        = 'ID';
317
				$where_sql = Settings::get_blacklisted_post_types_sql();
318
319
				break;
320
			case 'comments':
321
				$table     = $wpdb->comments;
322
				$id        = 'comment_ID';
323
				$where_sql = Settings::get_comments_filter_sql();
324
				break;
325
		}
326
327
		// TODO: Call $wpdb->prepare on the following query.
328
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
329
		$results = $wpdb->get_results( "SELECT MAX({$id}) as max, MIN({$id}) as min, COUNT({$id}) as count FROM {$table} WHERE {$where_sql}" );
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $table does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $where_sql does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
330
		if ( isset( $results[0] ) ) {
331
			return $results[0];
332
		}
333
334
		return array();
335
	}
336
337
	/**
338
	 * Get the range for content (posts and comments) to sync.
339
	 *
340
	 * @access private
341
	 *
342
	 * @param array $config Full sync configuration for this all sync modules.
343
	 * @return array Array of range (min ID, max ID, total items) for all content types.
344
	 */
345
	private function get_content_range( $config ) {
346
		$range = array();
347
		// Only when we are sending the whole range do we want to send also the range.
348 View Code Duplication
		if ( true === isset( $config['posts'] ) && $config['posts'] ) {
349
			$range['posts'] = $this->get_range( 'posts' );
350
		}
351
352 View Code Duplication
		if ( true === isset( $config['comments'] ) && $config['comments'] ) {
353
			$range['comments'] = $this->get_range( 'comments' );
354
		}
355
		return $range;
356
	}
357
358
	/**
359
	 * Update the progress after sync modules actions have been processed on the server.
360
	 *
361
	 * @access public
362
	 *
363
	 * @param array $actions Actions that have been processed on the server.
364
	 */
365
	public function update_sent_progress_action( $actions ) {
366
		// Quick way to map to first items with an array of arrays.
367
		$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
368
369
		// Total item counts for each action.
370
		$actions_with_total_counts = $this->get_actions_totals( $actions );
371
372
		if ( ! $this->is_started() || $this->is_finished() ) {
373
			return;
374
		}
375
376
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
377
			$this->update_status_option( 'send_started', time() );
378
		}
379
380
		foreach ( Modules::get_modules() as $module ) {
0 ignored issues
show
Bug introduced by
The expression \Automattic\Jetpack\Sync\Modules::get_modules() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
381
			$module_actions     = $module->get_full_sync_actions();
382
			$status_option_name = "{$module->name()}_sent";
383
			$total_option_name  = "{$status_option_name}_total";
384
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
385
			$items_sent_total   = $this->get_status_option( $total_option_name, 0 );
386
387
			foreach ( $module_actions as $module_action ) {
388
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
389
					$items_sent += $actions_with_counts[ $module_action ];
390
				}
391
392
				if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
393
					$items_sent_total += $actions_with_total_counts[ $module_action ];
394
				}
395
			}
396
397
			if ( $items_sent > 0 ) {
398
				$this->update_status_option( $status_option_name, $items_sent );
399
			}
400
401
			if ( 0 !== $items_sent_total ) {
402
				$this->update_status_option( $total_option_name, $items_sent_total );
403
			}
404
		}
405
406
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
407
			$this->update_status_option( 'finished', time() );
408
		}
409
	}
410
411
	/**
412
	 * Get the name of the action for an item in the sync queue.
413
	 *
414
	 * @access public
415
	 *
416
	 * @param array $queue_item Item of the sync queue.
417
	 * @return string|boolean Name of the action, false if queue item is invalid.
418
	 */
419
	public function get_action_name( $queue_item ) {
420
		if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
421
			return $queue_item[0];
422
		}
423
		return false;
424
	}
425
426
	/**
427
	 * Retrieve the total number of items we're syncing in a particular queue item (action).
428
	 * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
429
	 * represents the first (and only) chunk of items to sync in that action.
430
	 *
431
	 * @access public
432
	 *
433
	 * @param array $queue_item Item of the sync queue that corresponds to a particular action.
434
	 * @return int Total number of items in the action.
435
	 */
436
	public function get_action_totals( $queue_item ) {
437
		if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
438
			if ( is_array( $queue_item[1][0] ) ) {
439
				// Let's count the items we sync in this action.
440
				return count( $queue_item[1][0] );
441
			}
442
			// -1 indicates that this action syncs all items by design.
443
			return -1;
444
		}
445
		return 0;
446
	}
447
448
	/**
449
	 * Retrieve the total number of items for a set of actions, grouped by action name.
450
	 *
451
	 * @access public
452
	 *
453
	 * @param array $actions An array of actions.
454
	 * @return array An array, representing the total number of items, grouped per action.
455
	 */
456
	public function get_actions_totals( $actions ) {
457
		$totals = array();
458
459
		foreach ( $actions as $action ) {
460
			$name          = $this->get_action_name( $action );
461
			$action_totals = $this->get_action_totals( $action );
462
			if ( ! isset( $totals[ $name ] ) ) {
463
				$totals[ $name ] = 0;
464
			}
465
			$totals[ $name ] += $action_totals;
466
		}
467
468
		return $totals;
469
	}
470
471
	/**
472
	 * Whether full sync has started.
473
	 *
474
	 * @access public
475
	 *
476
	 * @return boolean
477
	 */
478
	public function is_started() {
479
		return ! ! $this->get_status_option( 'started' );
480
	}
481
482
	/**
483
	 * Whether full sync has finished.
484
	 *
485
	 * @access public
486
	 *
487
	 * @return boolean
488
	 */
489
	public function is_finished() {
490
		return ! ! $this->get_status_option( 'finished' );
491
	}
492
493
	/**
494
	 * Retrieve the status of the current full sync.
495
	 *
496
	 * @access public
497
	 *
498
	 * @return array Full sync status.
499
	 */
500
	public function get_status() {
501
		$status = array(
502
			'started'        => $this->get_status_option( 'started' ),
503
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
504
			'send_started'   => $this->get_status_option( 'send_started' ),
505
			'finished'       => $this->get_status_option( 'finished' ),
506
			'sent'           => array(),
507
			'sent_total'     => array(),
508
			'queue'          => array(),
509
			'config'         => $this->get_status_option( 'params' ),
510
			'total'          => array(),
511
		);
512
513
		$enqueue_status = $this->get_enqueue_status();
514
515
		foreach ( Modules::get_modules() as $module ) {
0 ignored issues
show
Bug introduced by
The expression \Automattic\Jetpack\Sync\Modules::get_modules() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
516
			$name = $module->name();
517
518
			if ( ! isset( $enqueue_status[ $name ] ) ) {
519
				continue;
520
			}
521
522
			list( $total, $queued ) = $enqueue_status[ $name ];
523
524
			if ( $total ) {
525
				$status['total'][ $name ] = $total;
526
			}
527
528
			if ( $queued ) {
529
				$status['queue'][ $name ] = $queued;
530
			}
531
532
			$sent = $this->get_status_option( "{$name}_sent" );
533
			if ( $sent ) {
534
				$status['sent'][ $name ] = $sent;
535
			}
536
537
			$sent_total = $this->get_status_option( "{$name}_sent_total" );
538
			if ( $sent_total ) {
539
				$status['sent_total'][ $name ] = $sent_total;
540
			}
541
		}
542
543
		return $status;
544
	}
545
546
	/**
547
	 * Clear all the full sync status options.
548
	 *
549
	 * @access public
550
	 */
551
	public function clear_status() {
552
		$prefix = self::STATUS_OPTION_PREFIX;
553
		\Jetpack_Options::delete_raw_option( "{$prefix}_started" );
554
		\Jetpack_Options::delete_raw_option( "{$prefix}_params" );
555
		\Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
556
		\Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
557
		\Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
558
559
		$this->delete_enqueue_status();
560
561
		foreach ( Modules::get_modules() as $module ) {
0 ignored issues
show
Bug introduced by
The expression \Automattic\Jetpack\Sync\Modules::get_modules() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
562
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
563
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
564
		}
565
	}
566
567
	/**
568
	 * Clear all the full sync data.
569
	 *
570
	 * @access public
571
	 */
572
	public function reset_data() {
573
		$this->clear_status();
574
		$this->delete_config();
575
576
		$listener = Listener::get_instance();
577
		$listener->get_full_sync_queue()->reset();
578
	}
579
580
	/**
581
	 * Get the value of a full sync status option.
582
	 *
583
	 * @access private
584
	 *
585
	 * @param string $name    Name of the option.
586
	 * @param mixed  $default Default value of the option.
587
	 * @return mixed Option value.
588
	 */
589
	private function get_status_option( $name, $default = null ) {
590
		$value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
591
592
		return is_numeric( $value ) ? intval( $value ) : $value;
593
	}
594
595
	/**
596
	 * Update the value of a full sync status option.
597
	 *
598
	 * @access private
599
	 *
600
	 * @param string  $name     Name of the option.
601
	 * @param mixed   $value    Value of the option.
602
	 * @param boolean $autoload Whether the option should be autoloaded at the beginning of the request.
603
	 */
604
	private function update_status_option( $name, $value, $autoload = false ) {
605
		\Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
606
	}
607
608
	/**
609
	 * Set the full sync enqueue status.
610
	 *
611
	 * @access private
612
	 *
613
	 * @param array $new_status The new full sync enqueue status.
614
	 */
615
	private function set_enqueue_status( $new_status ) {
616
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
617
	}
618
619
	/**
620
	 * Delete full sync enqueue status.
621
	 *
622
	 * @access private
623
	 *
624
	 * @return boolean Whether the status was deleted.
625
	 */
626
	private function delete_enqueue_status() {
627
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
628
	}
629
630
	/**
631
	 * Retrieve the current full sync enqueue status.
632
	 *
633
	 * @access private
634
	 *
635
	 * @return array Full sync enqueue status.
636
	 */
637
	public function get_enqueue_status() {
638
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
639
	}
640
641
	/**
642
	 * Set the full sync enqueue configuration.
643
	 *
644
	 * @access private
645
	 *
646
	 * @param array $config The new full sync enqueue configuration.
647
	 */
648
	private function set_config( $config ) {
649
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
650
	}
651
652
	/**
653
	 * Delete full sync configuration.
654
	 *
655
	 * @access private
656
	 *
657
	 * @return boolean Whether the configuration was deleted.
658
	 */
659
	private function delete_config() {
660
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
661
	}
662
663
	/**
664
	 * Retrieve the current full sync enqueue config.
665
	 *
666
	 * @access private
667
	 *
668
	 * @return array Full sync enqueue config.
669
	 */
670
	private function get_config() {
671
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
672
	}
673
674
	/**
675
	 * Update an option manually to bypass filters and caching.
676
	 *
677
	 * @access private
678
	 *
679
	 * @param string $name  Option name.
680
	 * @param mixed  $value Option value.
681
	 * @return int The number of updated rows in the database.
682
	 */
683
	private function write_option( $name, $value ) {
684
		// We write our own option updating code to bypass filters/caching/etc on set_option/get_option.
685
		global $wpdb;
686
		$serialized_value = maybe_serialize( $value );
687
688
		/**
689
		 * Try updating, if no update then insert
690
		 * TODO: try to deal with the fact that unchanged values can return updated_num = 0
691
		 * below we used "insert ignore" to at least suppress the resulting error.
692
		 */
693
		$updated_num = $wpdb->query(
694
			$wpdb->prepare(
695
				"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
696
				$serialized_value,
697
				$name
698
			)
699
		);
700
701
		if ( ! $updated_num ) {
702
			$updated_num = $wpdb->query(
703
				$wpdb->prepare(
704
					"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
705
					$name,
706
					$serialized_value
707
				)
708
			);
709
		}
710
		return $updated_num;
711
	}
712
713
	/**
714
	 * Update an option manually to bypass filters and caching.
715
	 *
716
	 * @access private
717
	 *
718
	 * @param string $name    Option name.
719
	 * @param mixed  $default Default option value.
720
	 * @return mixed Option value.
721
	 */
722
	private function read_option( $name, $default = null ) {
723
		global $wpdb;
724
		$value = $wpdb->get_var(
725
			$wpdb->prepare(
726
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
727
				$name
728
			)
729
		);
730
		$value = maybe_unserialize( $value );
731
732
		if ( null === $value && null !== $default ) {
733
			return $default;
734
		}
735
736
		return $value;
737
	}
738
739
	/**
740
	 * Prefix of the blog lock transient.
741
	 *
742
	 * @access public
743
	 *
744
	 * @var string
745
	 */
746
	const ENQUEUE_LOCK_TRANSIENT_PREFIX = 'jp_sync_enqueue_lock_';
747
	/**
748
	 * Lifetime of the blog lock transient.
749
	 *
750
	 * @access public
751
	 *
752
	 * @var int
753
	 */
754
	const ENQUEUE_LOCK_TRANSIENT_EXPIRY = 15; // Seconds.
755
756
	/**
757
	 * Attempt to lock enqueueing when the server receives concurrent requests from the same blog.
758
	 *
759
	 * @access public
760
	 *
761
	 * @param int $expiry  enqueue transient lifetime.
762
	 * @return boolean True if succeeded, false otherwise.
763
	 */
764 View Code Duplication
	public function attempt_enqueue_lock( $expiry = self::ENQUEUE_LOCK_TRANSIENT_EXPIRY ) {
765
		$transient_name = $this->get_concurrent_enqueue_transient_name();
766
		$locked_time    = get_site_transient( $transient_name );
767
		if ( $locked_time ) {
768
			return false;
769
		}
770
		set_site_transient( $transient_name, microtime( true ), $expiry );
771
		return true;
772
	}
773
	/**
774
	 * Retrieve the enqueue lock transient name for the current blog.
775
	 *
776
	 * @access public
777
	 *
778
	 * @return string Name of the blog lock transient.
779
	 */
780
	private function get_concurrent_enqueue_transient_name() {
781
		return self::ENQUEUE_LOCK_TRANSIENT_PREFIX . get_current_blog_id();
782
	}
783
	/**
784
	 * Remove the enqueue lock.
785
	 *
786
	 * @access public
787
	 */
788
	public function remove_enqueue_lock() {
789
		delete_site_transient( $this->get_concurrent_enqueue_transient_name() );
790
	}
791
}
792