Completed
Push — fix/check-queue-status-before-... ( b798e5...090130 )
by
unknown
06:51
created

Full_Sync   F

Complexity

Total Complexity 96

Size/Duplication

Total Lines 747
Duplicated Lines 2.14 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 16
loc 747
rs 1.853
c 0
b 0
f 0
wmc 96
lcom 1
cbo 6

30 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 3 1
A init_full_sync_listeners() 0 6 1
A init_before_send() 0 4 1
C start() 0 84 14
A continue_enqueuing() 0 7 2
D continue_enqueuing_with_lock() 0 84 16
A get_range() 0 29 5
A get_content_range() 6 12 5
B update_sent_progress_action() 0 45 11
A get_action_name() 0 6 3
A get_action_totals() 0 11 4
A get_actions_totals() 0 14 3
A is_started() 0 3 1
A is_finished() 0 3 1
B get_status() 0 45 7
A clear_status() 0 15 2
A reset_data() 0 7 1
A get_status_option() 0 5 2
A update_status_option() 0 3 1
A set_enqueue_status() 0 3 1
A delete_enqueue_status() 0 3 1
A get_enqueue_status() 0 3 1
A set_config() 0 3 1
A delete_config() 0 3 1
A get_config() 0 3 1
A write_option() 0 29 2
A read_option() 0 16 3
A attempt_enqueue_lock() 9 9 2
A get_concurrent_enqueue_transient_name() 0 3 1
A remove_enqueue_lock() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Full_Sync often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Full_Sync, and based on these observations, apply Extract Interface, too.

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