Completed
Push — fix/check-queue-status-before-... ( bc8184...0d46e6 )
by
unknown
06:46
created

Full_Sync::get_action_totals()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 11
rs 9.9
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, $enqueue_status );
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
	 * @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...
178
	 */
179
	public function continue_enqueuing( $configs = null, $enqueue_status = null ) {
180
		if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) {
181
			return;
182
		}
183
184
		$listener        = Listener::get_instance();
185
		$full_sync_queue = new Queue( 'full_sync' );
186
187
		/*
188
		* Periodically check the size of the queue, and disable adding to it if
189
		* it exceeds some limit AND the oldest item exceeds the age limit (i.e. sending has stopped).
190
		*/
191
		if ( ! $listener->can_add_to_queue( $full_sync_queue ) ) {
192
			return;
193
		}
194
195
		if ( ! $this->attempt_enqueue_lock() ) {
196
			return;
197
		}
198
		$this->continue_enqueuing_with_lock( $configs, $enqueue_status, $full_sync_queue );
0 ignored issues
show
Bug introduced by
It seems like $configs defined by parameter $configs on line 179 can also be of type null; however, Automattic\Jetpack\Sync\...e_enqueuing_with_lock() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $enqueue_status defined by parameter $enqueue_status on line 179 can also be of type null; however, Automattic\Jetpack\Sync\...e_enqueuing_with_lock() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
199
		$this->remove_enqueue_lock();
200
	}
201
202
	/**
203
	 * Enqueue the next items to sync. Once we have a lock.
204
	 *
205
	 * @param array  $configs Full sync configuration for all sync modules.
206
	 * @param array  $enqueue_status Current status of the queue, indexed by sync modules.
207
	 * @param Object $full_sync_queue The full sync queue.
208
	 */
209
	private function continue_enqueuing_with_lock( $configs, $enqueue_status, $full_sync_queue ) {
210
		if ( ! $configs ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $configs 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...
211
			$configs = $this->get_config();
212
		}
213
214
		if ( ! $enqueue_status ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $enqueue_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...
215
			$enqueue_status = $this->get_enqueue_status();
216
		}
217
218
		// If full sync queue is full, don't enqueue more items.
219
		$max_queue_size_full_sync   = Settings::get_setting( 'max_queue_size_full_sync' );
220
		$available_queue_slots      = $max_queue_size_full_sync - $full_sync_queue->size();
221
		$remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots );
222
223
		$modules           = Modules::get_modules();
224
		$modules_processed = 0;
225
		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...
226
			$module_name = $module->name();
227
228
			// Skip module if not configured for this sync or module is done.
229
			if ( ! isset( $configs[ $module_name ] )
230
				 || // No module config.
231
				 ! $configs[ $module_name ]
232
				 || // No enqueue status.
233
				 ! $enqueue_status[ $module_name ]
234
				 || // Finished enqueuing this module.
235
				 true === $enqueue_status[ $module_name ][2] ) {
236
				$modules_processed ++;
237
				continue;
238
			}
239
240
			list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][2] );
241
242
			$enqueue_status[ $module_name ][2] = $next_enqueue_state;
243
244
			// If items were processed, subtract them from the limit.
245
			if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
246
				$enqueue_status[ $module_name ][1] += $items_enqueued;
247
				$remaining_items_to_enqueue        -= $items_enqueued;
248
			}
249
250
			if ( true === $next_enqueue_state ) {
251
				$modules_processed ++;
252
			}
253
			// Stop processing if we've reached our limit of items to enqueue.
254
			if ( 0 >= $remaining_items_to_enqueue ) {
255
				break;
256
			}
257
		}
258
259
		$this->set_enqueue_status( $enqueue_status );
260
261
		if ( count( $modules ) > $modules_processed ) {
262
			return;
263
		}
264
265
		// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
266
		$this->update_status_option( 'queue_finished', time(), true );
267
268
		$range = $this->get_content_range( $configs );
269
270
		/**
271
		 * Fires when a full sync ends. This action is serialized
272
		 * and sent to the server.
273
		 *
274
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
275
		 * @param array $range Range of the sync items, containing min and max IDs for some item types.
276
		 *
277
		 * @since 4.2.0
278
		 * @since 7.3.0 Added $range arg.
279
		 */
280
		do_action( 'jetpack_full_sync_end', '', $range );
281
	}
282
283
	/**
284
	 * Get the range (min ID, max ID and total items) of items to sync.
285
	 *
286
	 * @access public
287
	 *
288
	 * @param string $type Type of sync item to get the range for.
289
	 * @return array Array of min ID, max ID and total items in the range.
290
	 */
291
	public function get_range( $type ) {
292
		global $wpdb;
293
		if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
294
			return array();
295
		}
296
297
		switch ( $type ) {
298
			case 'posts':
299
				$table     = $wpdb->posts;
300
				$id        = 'ID';
301
				$where_sql = Settings::get_blacklisted_post_types_sql();
302
303
				break;
304
			case 'comments':
305
				$table     = $wpdb->comments;
306
				$id        = 'comment_ID';
307
				$where_sql = Settings::get_comments_filter_sql();
308
				break;
309
		}
310
311
		// TODO: Call $wpdb->prepare on the following query.
312
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
313
		$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...
314
		if ( isset( $results[0] ) ) {
315
			return $results[0];
316
		}
317
318
		return array();
319
	}
320
321
	/**
322
	 * Get the range for content (posts and comments) to sync.
323
	 *
324
	 * @access private
325
	 *
326
	 * @param array $config Full sync configuration for this all sync modules.
327
	 * @return array Array of range (min ID, max ID, total items) for all content types.
328
	 */
329
	private function get_content_range( $config ) {
330
		$range = array();
331
		// Only when we are sending the whole range do we want to send also the range.
332 View Code Duplication
		if ( true === isset( $config['posts'] ) && $config['posts'] ) {
333
			$range['posts'] = $this->get_range( 'posts' );
334
		}
335
336 View Code Duplication
		if ( true === isset( $config['comments'] ) && $config['comments'] ) {
337
			$range['comments'] = $this->get_range( 'comments' );
338
		}
339
		return $range;
340
	}
341
342
	/**
343
	 * Update the progress after sync modules actions have been processed on the server.
344
	 *
345
	 * @access public
346
	 *
347
	 * @param array $actions Actions that have been processed on the server.
348
	 */
349
	public function update_sent_progress_action( $actions ) {
350
		// Quick way to map to first items with an array of arrays.
351
		$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
352
353
		// Total item counts for each action.
354
		$actions_with_total_counts = $this->get_actions_totals( $actions );
355
356
		if ( ! $this->is_started() || $this->is_finished() ) {
357
			return;
358
		}
359
360
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
361
			$this->update_status_option( 'send_started', time() );
362
		}
363
364
		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...
365
			$module_actions     = $module->get_full_sync_actions();
366
			$status_option_name = "{$module->name()}_sent";
367
			$total_option_name  = "{$status_option_name}_total";
368
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
369
			$items_sent_total   = $this->get_status_option( $total_option_name, 0 );
370
371
			foreach ( $module_actions as $module_action ) {
372
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
373
					$items_sent += $actions_with_counts[ $module_action ];
374
				}
375
376
				if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
377
					$items_sent_total += $actions_with_total_counts[ $module_action ];
378
				}
379
			}
380
381
			if ( $items_sent > 0 ) {
382
				$this->update_status_option( $status_option_name, $items_sent );
383
			}
384
385
			if ( 0 !== $items_sent_total ) {
386
				$this->update_status_option( $total_option_name, $items_sent_total );
387
			}
388
		}
389
390
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
391
			$this->update_status_option( 'finished', time() );
392
		}
393
	}
394
395
	/**
396
	 * Get the name of the action for an item in the sync queue.
397
	 *
398
	 * @access public
399
	 *
400
	 * @param array $queue_item Item of the sync queue.
401
	 * @return string|boolean Name of the action, false if queue item is invalid.
402
	 */
403
	public function get_action_name( $queue_item ) {
404
		if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
405
			return $queue_item[0];
406
		}
407
		return false;
408
	}
409
410
	/**
411
	 * Retrieve the total number of items we're syncing in a particular queue item (action).
412
	 * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
413
	 * represents the first (and only) chunk of items to sync in that action.
414
	 *
415
	 * @access public
416
	 *
417
	 * @param array $queue_item Item of the sync queue that corresponds to a particular action.
418
	 * @return int Total number of items in the action.
419
	 */
420
	public function get_action_totals( $queue_item ) {
421
		if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
422
			if ( is_array( $queue_item[1][0] ) ) {
423
				// Let's count the items we sync in this action.
424
				return count( $queue_item[1][0] );
425
			}
426
			// -1 indicates that this action syncs all items by design.
427
			return -1;
428
		}
429
		return 0;
430
	}
431
432
	/**
433
	 * Retrieve the total number of items for a set of actions, grouped by action name.
434
	 *
435
	 * @access public
436
	 *
437
	 * @param array $actions An array of actions.
438
	 * @return array An array, representing the total number of items, grouped per action.
439
	 */
440
	public function get_actions_totals( $actions ) {
441
		$totals = array();
442
443
		foreach ( $actions as $action ) {
444
			$name          = $this->get_action_name( $action );
445
			$action_totals = $this->get_action_totals( $action );
446
			if ( ! isset( $totals[ $name ] ) ) {
447
				$totals[ $name ] = 0;
448
			}
449
			$totals[ $name ] += $action_totals;
450
		}
451
452
		return $totals;
453
	}
454
455
	/**
456
	 * Whether full sync has started.
457
	 *
458
	 * @access public
459
	 *
460
	 * @return boolean
461
	 */
462
	public function is_started() {
463
		return ! ! $this->get_status_option( 'started' );
464
	}
465
466
	/**
467
	 * Whether full sync has finished.
468
	 *
469
	 * @access public
470
	 *
471
	 * @return boolean
472
	 */
473
	public function is_finished() {
474
		return ! ! $this->get_status_option( 'finished' );
475
	}
476
477
	/**
478
	 * Retrieve the status of the current full sync.
479
	 *
480
	 * @access public
481
	 *
482
	 * @return array Full sync status.
483
	 */
484
	public function get_status() {
485
		$status = array(
486
			'started'        => $this->get_status_option( 'started' ),
487
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
488
			'send_started'   => $this->get_status_option( 'send_started' ),
489
			'finished'       => $this->get_status_option( 'finished' ),
490
			'sent'           => array(),
491
			'sent_total'     => array(),
492
			'queue'          => array(),
493
			'config'         => $this->get_status_option( 'params' ),
494
			'total'          => array(),
495
		);
496
497
		$enqueue_status = $this->get_enqueue_status();
498
499
		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...
500
			$name = $module->name();
501
502
			if ( ! isset( $enqueue_status[ $name ] ) ) {
503
				continue;
504
			}
505
506
			list( $total, $queued ) = $enqueue_status[ $name ];
507
508
			if ( $total ) {
509
				$status['total'][ $name ] = $total;
510
			}
511
512
			if ( $queued ) {
513
				$status['queue'][ $name ] = $queued;
514
			}
515
516
			$sent = $this->get_status_option( "{$name}_sent" );
517
			if ( $sent ) {
518
				$status['sent'][ $name ] = $sent;
519
			}
520
521
			$sent_total = $this->get_status_option( "{$name}_sent_total" );
522
			if ( $sent_total ) {
523
				$status['sent_total'][ $name ] = $sent_total;
524
			}
525
		}
526
527
		return $status;
528
	}
529
530
	/**
531
	 * Clear all the full sync status options.
532
	 *
533
	 * @access public
534
	 */
535
	public function clear_status() {
536
		$prefix = self::STATUS_OPTION_PREFIX;
537
		\Jetpack_Options::delete_raw_option( "{$prefix}_started" );
538
		\Jetpack_Options::delete_raw_option( "{$prefix}_params" );
539
		\Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
540
		\Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
541
		\Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
542
543
		$this->delete_enqueue_status();
544
545
		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...
546
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
547
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
548
		}
549
	}
550
551
	/**
552
	 * Clear all the full sync data.
553
	 *
554
	 * @access public
555
	 */
556
	public function reset_data() {
557
		$this->clear_status();
558
		$this->delete_config();
559
560
		$listener = Listener::get_instance();
561
		$listener->get_full_sync_queue()->reset();
562
	}
563
564
	/**
565
	 * Get the value of a full sync status option.
566
	 *
567
	 * @access private
568
	 *
569
	 * @param string $name    Name of the option.
570
	 * @param mixed  $default Default value of the option.
571
	 * @return mixed Option value.
572
	 */
573
	private function get_status_option( $name, $default = null ) {
574
		$value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
575
576
		return is_numeric( $value ) ? intval( $value ) : $value;
577
	}
578
579
	/**
580
	 * Update the value of a full sync status option.
581
	 *
582
	 * @access private
583
	 *
584
	 * @param string  $name     Name of the option.
585
	 * @param mixed   $value    Value of the option.
586
	 * @param boolean $autoload Whether the option should be autoloaded at the beginning of the request.
587
	 */
588
	private function update_status_option( $name, $value, $autoload = false ) {
589
		\Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
590
	}
591
592
	/**
593
	 * Set the full sync enqueue status.
594
	 *
595
	 * @access private
596
	 *
597
	 * @param array $new_status The new full sync enqueue status.
598
	 */
599
	private function set_enqueue_status( $new_status ) {
600
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
601
	}
602
603
	/**
604
	 * Delete full sync enqueue status.
605
	 *
606
	 * @access private
607
	 *
608
	 * @return boolean Whether the status was deleted.
609
	 */
610
	private function delete_enqueue_status() {
611
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
612
	}
613
614
	/**
615
	 * Retrieve the current full sync enqueue status.
616
	 *
617
	 * @access private
618
	 *
619
	 * @return array Full sync enqueue status.
620
	 */
621
	public function get_enqueue_status() {
622
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
623
	}
624
625
	/**
626
	 * Set the full sync enqueue configuration.
627
	 *
628
	 * @access private
629
	 *
630
	 * @param array $config The new full sync enqueue configuration.
631
	 */
632
	private function set_config( $config ) {
633
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
634
	}
635
636
	/**
637
	 * Delete full sync configuration.
638
	 *
639
	 * @access private
640
	 *
641
	 * @return boolean Whether the configuration was deleted.
642
	 */
643
	private function delete_config() {
644
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
645
	}
646
647
	/**
648
	 * Retrieve the current full sync enqueue config.
649
	 *
650
	 * @access private
651
	 *
652
	 * @return array Full sync enqueue config.
653
	 */
654
	private function get_config() {
655
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
656
	}
657
658
659
	/**
660
	 * Prefix of the blog lock transient.
661
	 *
662
	 * @access public
663
	 *
664
	 * @var string
665
	 */
666
	const ENQUEUE_LOCK_TRANSIENT_PREFIX = 'jp_sync_enqueue_lock_';
667
668
	/**
669
	 * Lifetime of the blog lock transient.
670
	 *
671
	 * @access public
672
	 *
673
	 * @var int
674
	 */
675
	const ENQUEUE_LOCK_TRANSIENT_EXPIRY = 15; // Seconds.
676
677
	/**
678
	 * Attempt to lock enqueueing when the server receives concurrent requests from the same blog.
679
	 *
680
	 * @access public
681
	 *
682
	 * @param int $expiry  enqueue transient lifetime.
683
	 * @return boolean True if succeeded, false otherwise.
684
	 */
685 View Code Duplication
	public function attempt_enqueue_lock( $expiry = self::ENQUEUE_LOCK_TRANSIENT_EXPIRY ) {
686
		$transient_name = $this->get_concurrent_enqueue_transient_name();
687
		$locked_time    = get_site_transient( $transient_name );
688
		if ( $locked_time ) {
689
			return false;
690
		}
691
		set_site_transient( $transient_name, microtime( true ), $expiry );
692
693
		return true;
694
	}
695
696
	/**
697
	 * Retrieve the enqueue lock transient name for the current blog.
698
	 *
699
	 * @access public
700
	 *
701
	 * @return string Name of the blog lock transient.
702
	 */
703
	private function get_concurrent_enqueue_transient_name() {
704
		return self::ENQUEUE_LOCK_TRANSIENT_PREFIX . get_current_blog_id();
705
	}
706
707
	/**
708
	 * Remove the enqueue lock.
709
	 *
710
	 * @access public
711
	 */
712
	public function remove_enqueue_lock() {
713
		delete_site_transient( $this->get_concurrent_enqueue_transient_name() );
714
	}
715
716
}
717