Completed
Push — fix/check-queue-status-before-... ( 090130...46260c )
by
unknown
07:03
created

Full_Sync::is_finished()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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
	 *
45
	 * Remaining Items to enqueue.
46
	 *
47
	 * @var int
48
	 */
49
	private $remaining_items_to_enqueue = 0;
50
51
	/**
52
	 *
53
	 * Per each module: total items to send, how many have been enqueued, the last object_id enqueued
54
	 *
55
	 * @var array
56
	 */
57
	private $enqueue_status;
58
59
	/**
60
	 * Sync module name.
61
	 *
62
	 * @access public
63
	 *
64
	 * @return string
65
	 */
66
	public function name() {
67
		return 'full-sync';
68
	}
69
70
	/**
71
	 * Initialize action listeners for full sync.
72
	 *
73
	 * @access public
74
	 *
75
	 * @param callable $callable Action handler callable.
76
	 */
77
	public function init_full_sync_listeners( $callable ) {
78
		// Synthetic actions for full sync.
79
		add_action( 'jetpack_full_sync_start', $callable, 10, 3 );
80
		add_action( 'jetpack_full_sync_end', $callable, 10, 2 );
81
		add_action( 'jetpack_full_sync_cancelled', $callable );
82
	}
83
84
	/**
85
	 * Initialize the module in the sender.
86
	 *
87
	 * @access public
88
	 */
89
	public function init_before_send() {
90
		// This is triggered after actions have been processed on the server.
91
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
92
	}
93
94
	/**
95
	 * Start a full sync.
96
	 *
97
	 * @access public
98
	 *
99
	 * @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...
100
	 * @return bool Always returns true at success.
101
	 */
102
	public function start( $module_configs = null ) {
103
		$was_already_running = $this->is_started() && ! $this->is_finished();
104
105
		// Remove all evidence of previous full sync items and status.
106
		$this->reset_data();
107
108
		if ( $was_already_running ) {
109
			/**
110
			 * Fires when a full sync is cancelled.
111
			 *
112
			 * @since 4.2.0
113
			 */
114
			do_action( 'jetpack_full_sync_cancelled' );
115
		}
116
117
		$this->update_status_option( 'started', time() );
118
		$this->update_status_option( 'params', $module_configs );
119
120
		$enqueue_status   = array();
121
		$full_sync_config = array();
122
		$include_empty    = false;
123
		$empty            = array();
124
125
		// Default value is full sync.
126
		if ( ! is_array( $module_configs ) ) {
127
			$module_configs = array();
128
			$include_empty  = true;
129
			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...
130
				$module_configs[ $module->name() ] = true;
131
			}
132
		}
133
134
		// Set default configuration, calculate totals, and save configuration if totals > 0.
135
		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...
136
			$module_name   = $module->name();
137
			$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
138
139
			if ( ! $module_config ) {
140
				continue;
141
			}
142
143
			if ( 'users' === $module_name && 'initial' === $module_config ) {
144
				$module_config = $module->get_initial_sync_user_config();
145
			}
146
147
			$enqueue_status[ $module_name ] = false;
148
149
			$total_items = $module->estimate_full_sync_actions( $module_config );
150
151
			// If there's information to process, configure this module.
152
			if ( ! is_null( $total_items ) && $total_items > 0 ) {
153
				$full_sync_config[ $module_name ] = $module_config;
154
				$enqueue_status[ $module_name ]   = array(
155
					$total_items,   // Total.
156
					0,              // Queued.
157
					false,          // Current state.
158
				);
159
			} elseif ( $include_empty && 0 === $total_items ) {
160
				$empty[ $module_name ] = true;
161
			}
162
		}
163
164
		$this->set_config( $full_sync_config );
165
		$this->set_enqueue_status( $enqueue_status );
166
167
		$range = $this->get_content_range( $full_sync_config );
168
		/**
169
		 * Fires when a full sync begins. This action is serialized
170
		 * and sent to the server so that it knows a full sync is coming.
171
		 *
172
		 * @since 4.2.0
173
		 * @since 7.3.0 Added $range arg.
174
		 * @since 7.4.0 Added $empty arg.
175
		 *
176
		 * @param array $full_sync_config Sync configuration for all sync modules.
177
		 * @param array $range            Range of the sync items, containing min and max IDs for some item types.
178
		 * @param array $empty            The modules with no items to sync during a full sync.
179
		 */
180
		do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty );
181
182
		$this->continue_enqueuing( $full_sync_config, $enqueue_status );
183
184
		return true;
185
	}
186
187
	/**
188
	 * Enqueue the next items to sync.
189
	 *
190
	 * @access public
191
	 *
192
	 * @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...
193
	 * @param array $enqueue_status Current status of the queue, indexed by sync modules.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $enqueue_status 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...
194
	 */
195
	public function continue_enqueuing( $configs = null, $enqueue_status = null ) {
196
		if ( ! $this->attempt_enqueue_lock() ) {
197
			return;
198
		}
199
200
		$this->enqueue_status = $enqueue_status ? $enqueue_status : $this->get_enqueue_status();
201
		$this->continue_enqueuing_with_lock( $configs );
202
		$this->set_enqueue_status( $this->enqueue_status );
203
204
		$this->remove_enqueue_lock();
205
	}
206
207
	/**
208
	 *
209
	 * Get Available Full Sync queue slots.
210
	 *
211
	 * @return int
212
	 */
213
	public function get_available_queue_slots() {
214
		// If full sync queue is full, don't enqueue more items.
215
		$max_queue_size_full_sync = Settings::get_setting( 'max_queue_size_full_sync' );
216
		$full_sync_queue          = new Queue( 'full_sync' );
217
218
		return $max_queue_size_full_sync - $full_sync_queue->size();
219
	}
220
221
	/**
222
	 * Enqueue the next items to sync.
223
	 *
224
	 * @access public
225
	 *
226
	 * @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...
227
	 */
228
	public function continue_enqueuing_with_lock( $configs = null ) {
229
		if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) {
230
			return;
231
		}
232
233
		$this->remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $this->get_available_queue_slots() );
234
235
		if ( $this->remaining_items_to_enqueue <= 0 ) {
236
			return;
237
		}
238
239
		if ( ! $configs ) {
240
			$configs = $this->get_config();
241
		}
242
243
		$modules           = Modules::get_modules();
244
		$modules_processed = 0;
245
		foreach ( $modules as $module ) {
0 ignored issues
show
Bug introduced by
The expression $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...
246
			$modules_processed += $this->enqueue_module( $module, $configs[ $module->name() ], $this->enqueue_status[ $module->name() ], $this->remaining_items_to_enqueue );
0 ignored issues
show
Unused Code introduced by
The call to Full_Sync::enqueue_module() has too many arguments starting with $this->remaining_items_to_enqueue.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
247
			// Stop processing if we've reached our limit of items to enqueue.
248
249
			if ( 0 >= $this->remaining_items_to_enqueue ) {
250
				break;
251
			}
252
		}
253
254
		if ( count( $modules ) > $modules_processed ) {
255
			return;
256
		}
257
258
		// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
259
		$this->update_status_option( 'queue_finished', time(), true );
260
261
		$range = $this->get_content_range( $configs );
262
263
		/**
264
		 * Fires when a full sync ends. This action is serialized
265
		 * and sent to the server.
266
		 *
267
		 * @since 4.2.0
268
		 * @since 7.3.0 Added $range arg.
269
		 *
270
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
271
		 * @param array  $range    Range of the sync items, containing min and max IDs for some item types.
272
		 */
273
		do_action( 'jetpack_full_sync_end', '', $range );
274
	}
275
276
	/**
277
	 * Enqueue Full Sync Actions for the given module.
278
	 *
279
	 * @param Object $module The module to Enqueue.
280
	 * @param array  $config The Full sync configuration for the module.
281
	 * @param array  $status The Full sync enqueue status for the module.
282
	 *
283
	 * @return int
284
	 */
285
	public function enqueue_module( $module, $config, $status ) {
286
		// Skip module if not configured for this sync or module is done.
287
		if ( ! isset( $config )
288
			 || // No module config.
289
			 ! $config
0 ignored issues
show
Bug Best Practice introduced by
The expression $config of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
290
			 || // No enqueue status.
291
			 ! $status
0 ignored issues
show
Bug Best Practice introduced by
The expression $status of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
292
			 || // Finished enqueuing this module.
293
			 true === $status[2] ) {
294
			return 1;
295
		}
296
297
		list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $config, $this->remaining_items_to_enqueue, $status[2] );
298
299
		$status[2] = $next_enqueue_state;
300
301
		// If items were processed, subtract them from the limit.
302
		if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
303
			$status[1]                        += $items_enqueued;
304
			$this->remaining_items_to_enqueue -= $items_enqueued;
305
		}
306
307
		$this->enqueue_status[ $module->name() ] = $status;
308
309
		return true === $next_enqueue_state ? 1 : 0;
310
	}
311
312
	/**
313
	 * Get the range (min ID, max ID and total items) of items to sync.
314
	 *
315
	 * @access public
316
	 *
317
	 * @param string $type Type of sync item to get the range for.
318
	 * @return array Array of min ID, max ID and total items in the range.
319
	 */
320
	public function get_range( $type ) {
321
		global $wpdb;
322
		if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
323
			return array();
324
		}
325
326
		switch ( $type ) {
327
			case 'posts':
328
				$table     = $wpdb->posts;
329
				$id        = 'ID';
330
				$where_sql = Settings::get_blacklisted_post_types_sql();
331
332
				break;
333
			case 'comments':
334
				$table     = $wpdb->comments;
335
				$id        = 'comment_ID';
336
				$where_sql = Settings::get_comments_filter_sql();
337
				break;
338
		}
339
340
		// TODO: Call $wpdb->prepare on the following query.
341
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
342
		$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...
343
		if ( isset( $results[0] ) ) {
344
			return $results[0];
345
		}
346
347
		return array();
348
	}
349
350
	/**
351
	 * Get the range for content (posts and comments) to sync.
352
	 *
353
	 * @access private
354
	 *
355
	 * @param array $config Full sync configuration for this all sync modules.
356
	 * @return array Array of range (min ID, max ID, total items) for all content types.
357
	 */
358
	private function get_content_range( $config ) {
359
		$range = array();
360
		// Only when we are sending the whole range do we want to send also the range.
361 View Code Duplication
		if ( true === isset( $config['posts'] ) && $config['posts'] ) {
362
			$range['posts'] = $this->get_range( 'posts' );
363
		}
364
365 View Code Duplication
		if ( true === isset( $config['comments'] ) && $config['comments'] ) {
366
			$range['comments'] = $this->get_range( 'comments' );
367
		}
368
		return $range;
369
	}
370
371
	/**
372
	 * Update the progress after sync modules actions have been processed on the server.
373
	 *
374
	 * @access public
375
	 *
376
	 * @param array $actions Actions that have been processed on the server.
377
	 */
378
	public function update_sent_progress_action( $actions ) {
379
		// Quick way to map to first items with an array of arrays.
380
		$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
381
382
		// Total item counts for each action.
383
		$actions_with_total_counts = $this->get_actions_totals( $actions );
384
385
		if ( ! $this->is_started() || $this->is_finished() ) {
386
			return;
387
		}
388
389
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
390
			$this->update_status_option( 'send_started', time() );
391
		}
392
393
		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...
394
			$module_actions     = $module->get_full_sync_actions();
395
			$status_option_name = "{$module->name()}_sent";
396
			$total_option_name  = "{$status_option_name}_total";
397
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
398
			$items_sent_total   = $this->get_status_option( $total_option_name, 0 );
399
400
			foreach ( $module_actions as $module_action ) {
401
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
402
					$items_sent += $actions_with_counts[ $module_action ];
403
				}
404
405
				if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
406
					$items_sent_total += $actions_with_total_counts[ $module_action ];
407
				}
408
			}
409
410
			if ( $items_sent > 0 ) {
411
				$this->update_status_option( $status_option_name, $items_sent );
412
			}
413
414
			if ( 0 !== $items_sent_total ) {
415
				$this->update_status_option( $total_option_name, $items_sent_total );
416
			}
417
		}
418
419
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
420
			$this->update_status_option( 'finished', time() );
421
		}
422
	}
423
424
	/**
425
	 * Get the name of the action for an item in the sync queue.
426
	 *
427
	 * @access public
428
	 *
429
	 * @param array $queue_item Item of the sync queue.
430
	 * @return string|boolean Name of the action, false if queue item is invalid.
431
	 */
432
	public function get_action_name( $queue_item ) {
433
		if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
434
			return $queue_item[0];
435
		}
436
		return false;
437
	}
438
439
	/**
440
	 * Retrieve the total number of items we're syncing in a particular queue item (action).
441
	 * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
442
	 * represents the first (and only) chunk of items to sync in that action.
443
	 *
444
	 * @access public
445
	 *
446
	 * @param array $queue_item Item of the sync queue that corresponds to a particular action.
447
	 * @return int Total number of items in the action.
448
	 */
449
	public function get_action_totals( $queue_item ) {
450
		if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
451
			if ( is_array( $queue_item[1][0] ) ) {
452
				// Let's count the items we sync in this action.
453
				return count( $queue_item[1][0] );
454
			}
455
			// -1 indicates that this action syncs all items by design.
456
			return -1;
457
		}
458
		return 0;
459
	}
460
461
	/**
462
	 * Retrieve the total number of items for a set of actions, grouped by action name.
463
	 *
464
	 * @access public
465
	 *
466
	 * @param array $actions An array of actions.
467
	 * @return array An array, representing the total number of items, grouped per action.
468
	 */
469
	public function get_actions_totals( $actions ) {
470
		$totals = array();
471
472
		foreach ( $actions as $action ) {
473
			$name          = $this->get_action_name( $action );
474
			$action_totals = $this->get_action_totals( $action );
475
			if ( ! isset( $totals[ $name ] ) ) {
476
				$totals[ $name ] = 0;
477
			}
478
			$totals[ $name ] += $action_totals;
479
		}
480
481
		return $totals;
482
	}
483
484
	/**
485
	 * Whether full sync has started.
486
	 *
487
	 * @access public
488
	 *
489
	 * @return boolean
490
	 */
491
	public function is_started() {
492
		return ! ! $this->get_status_option( 'started' );
493
	}
494
495
	/**
496
	 * Whether full sync has finished.
497
	 *
498
	 * @access public
499
	 *
500
	 * @return boolean
501
	 */
502
	public function is_finished() {
503
		return ! ! $this->get_status_option( 'finished' );
504
	}
505
506
	/**
507
	 * Retrieve the status of the current full sync.
508
	 *
509
	 * @access public
510
	 *
511
	 * @return array Full sync status.
512
	 */
513
	public function get_status() {
514
		$status = array(
515
			'started'        => $this->get_status_option( 'started' ),
516
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
517
			'send_started'   => $this->get_status_option( 'send_started' ),
518
			'finished'       => $this->get_status_option( 'finished' ),
519
			'sent'           => array(),
520
			'sent_total'     => array(),
521
			'queue'          => array(),
522
			'config'         => $this->get_status_option( 'params' ),
523
			'total'          => array(),
524
		);
525
526
		$enqueue_status = $this->get_enqueue_status();
527
528
		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...
529
			$name = $module->name();
530
531
			if ( ! isset( $enqueue_status[ $name ] ) ) {
532
				continue;
533
			}
534
535
			list( $total, $queued ) = $enqueue_status[ $name ];
536
537
			if ( $total ) {
538
				$status['total'][ $name ] = $total;
539
			}
540
541
			if ( $queued ) {
542
				$status['queue'][ $name ] = $queued;
543
			}
544
545
			$sent = $this->get_status_option( "{$name}_sent" );
546
			if ( $sent ) {
547
				$status['sent'][ $name ] = $sent;
548
			}
549
550
			$sent_total = $this->get_status_option( "{$name}_sent_total" );
551
			if ( $sent_total ) {
552
				$status['sent_total'][ $name ] = $sent_total;
553
			}
554
		}
555
556
		return $status;
557
	}
558
559
	/**
560
	 * Clear all the full sync status options.
561
	 *
562
	 * @access public
563
	 */
564
	public function clear_status() {
565
		$prefix = self::STATUS_OPTION_PREFIX;
566
		\Jetpack_Options::delete_raw_option( "{$prefix}_started" );
567
		\Jetpack_Options::delete_raw_option( "{$prefix}_params" );
568
		\Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
569
		\Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
570
		\Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
571
572
		$this->delete_enqueue_status();
573
574
		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...
575
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
576
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
577
		}
578
	}
579
580
	/**
581
	 * Clear all the full sync data.
582
	 *
583
	 * @access public
584
	 */
585
	public function reset_data() {
586
		$this->clear_status();
587
		$this->delete_config();
588
589
		$listener = Listener::get_instance();
590
		$listener->get_full_sync_queue()->reset();
591
	}
592
593
	/**
594
	 * Get the value of a full sync status option.
595
	 *
596
	 * @access private
597
	 *
598
	 * @param string $name    Name of the option.
599
	 * @param mixed  $default Default value of the option.
600
	 * @return mixed Option value.
601
	 */
602
	private function get_status_option( $name, $default = null ) {
603
		$value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
604
605
		return is_numeric( $value ) ? intval( $value ) : $value;
606
	}
607
608
	/**
609
	 * Update the value of a full sync status option.
610
	 *
611
	 * @access private
612
	 *
613
	 * @param string  $name     Name of the option.
614
	 * @param mixed   $value    Value of the option.
615
	 * @param boolean $autoload Whether the option should be autoloaded at the beginning of the request.
616
	 */
617
	private function update_status_option( $name, $value, $autoload = false ) {
618
		\Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
619
	}
620
621
	/**
622
	 * Set the full sync enqueue status.
623
	 *
624
	 * @access private
625
	 *
626
	 * @param array $new_status The new full sync enqueue status.
627
	 */
628
	private function set_enqueue_status( $new_status ) {
629
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
630
	}
631
632
	/**
633
	 * Delete full sync enqueue status.
634
	 *
635
	 * @access private
636
	 *
637
	 * @return boolean Whether the status was deleted.
638
	 */
639
	private function delete_enqueue_status() {
640
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
641
	}
642
643
	/**
644
	 * Retrieve the current full sync enqueue status.
645
	 *
646
	 * @access private
647
	 *
648
	 * @return array Full sync enqueue status.
649
	 */
650
	public function get_enqueue_status() {
651
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
652
	}
653
654
	/**
655
	 * Set the full sync enqueue configuration.
656
	 *
657
	 * @access private
658
	 *
659
	 * @param array $config The new full sync enqueue configuration.
660
	 */
661
	private function set_config( $config ) {
662
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
663
	}
664
665
	/**
666
	 * Delete full sync configuration.
667
	 *
668
	 * @access private
669
	 *
670
	 * @return boolean Whether the configuration was deleted.
671
	 */
672
	private function delete_config() {
673
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
674
	}
675
676
	/**
677
	 * Retrieve the current full sync enqueue config.
678
	 *
679
	 * @access private
680
	 *
681
	 * @return array Full sync enqueue config.
682
	 */
683
	private function get_config() {
684
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
685
	}
686
687
	/**
688
	 * Update an option manually to bypass filters and caching.
689
	 *
690
	 * @access private
691
	 *
692
	 * @param string $name  Option name.
693
	 * @param mixed  $value Option value.
694
	 * @return int The number of updated rows in the database.
695
	 */
696
	private function write_option( $name, $value ) {
697
		// We write our own option updating code to bypass filters/caching/etc on set_option/get_option.
698
		global $wpdb;
699
		$serialized_value = maybe_serialize( $value );
700
701
		/**
702
		 * Try updating, if no update then insert
703
		 * TODO: try to deal with the fact that unchanged values can return updated_num = 0
704
		 * below we used "insert ignore" to at least suppress the resulting error.
705
		 */
706
		$updated_num = $wpdb->query(
707
			$wpdb->prepare(
708
				"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
709
				$serialized_value,
710
				$name
711
			)
712
		);
713
714
		if ( ! $updated_num ) {
715
			$updated_num = $wpdb->query(
716
				$wpdb->prepare(
717
					"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
718
					$name,
719
					$serialized_value
720
				)
721
			);
722
		}
723
		return $updated_num;
724
	}
725
726
	/**
727
	 * Update an option manually to bypass filters and caching.
728
	 *
729
	 * @access private
730
	 *
731
	 * @param string $name    Option name.
732
	 * @param mixed  $default Default option value.
733
	 * @return mixed Option value.
734
	 */
735
	private function read_option( $name, $default = null ) {
736
		global $wpdb;
737
		$value = $wpdb->get_var(
738
			$wpdb->prepare(
739
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
740
				$name
741
			)
742
		);
743
		$value = maybe_unserialize( $value );
744
745
		if ( null === $value && null !== $default ) {
746
			return $default;
747
		}
748
749
		return $value;
750
	}
751
752
	/**
753
	 * Prefix of the blog lock transient.
754
	 *
755
	 * @access public
756
	 *
757
	 * @var string
758
	 */
759
	const ENQUEUE_LOCK_TRANSIENT_PREFIX = 'jp_sync_enqueue_lock_';
760
	/**
761
	 * Lifetime of the blog lock transient.
762
	 *
763
	 * @access public
764
	 *
765
	 * @var int
766
	 */
767
	const ENQUEUE_LOCK_TRANSIENT_EXPIRY = 15; // Seconds.
768
769
	/**
770
	 * Attempt to lock enqueueing when the server receives concurrent requests from the same blog.
771
	 *
772
	 * @access public
773
	 *
774
	 * @param int $expiry  enqueue transient lifetime.
775
	 * @return boolean True if succeeded, false otherwise.
776
	 */
777 View Code Duplication
	public function attempt_enqueue_lock( $expiry = self::ENQUEUE_LOCK_TRANSIENT_EXPIRY ) {
778
		$transient_name = $this->get_concurrent_enqueue_transient_name();
779
		$locked_time    = get_site_transient( $transient_name );
780
		if ( $locked_time ) {
781
			return false;
782
		}
783
		set_site_transient( $transient_name, microtime( true ), $expiry );
784
		return true;
785
	}
786
	/**
787
	 * Retrieve the enqueue lock transient name for the current blog.
788
	 *
789
	 * @access public
790
	 *
791
	 * @return string Name of the blog lock transient.
792
	 */
793
	private function get_concurrent_enqueue_transient_name() {
794
		return self::ENQUEUE_LOCK_TRANSIENT_PREFIX . get_current_blog_id();
795
	}
796
	/**
797
	 * Remove the enqueue lock.
798
	 *
799
	 * @access public
800
	 */
801
	public function remove_enqueue_lock() {
802
		delete_site_transient( $this->get_concurrent_enqueue_transient_name() );
803
	}
804
}
805