Completed
Push — update/full-sync ( 7cf495 )
by
unknown
06:38
created

Full_Sync   F

Complexity

Total Complexity 88

Size/Duplication

Total Lines 684
Duplicated Lines 0.88 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 6
loc 684
rs 1.916
c 0
b 0
f 0
wmc 88
lcom 1
cbo 6

26 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
D start() 0 94 14
C continue_enqueuing() 0 85 14
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 16 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

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
	var $is_full_sync_capable = false;
29
30
	/**
31
	 * Prefix of the full sync status option name.
32
	 *
33
	 * @var string
34
	 */
35
	const STATUS_OPTION_PREFIX = 'jetpack_sync_full_';
36
37
	/**
38
	 * Sync module name.
39
	 *
40
	 * @access public
41
	 *
42
	 * @return string
43
	 */
44
	public function name() {
45
		return 'full-sync';
46
	}
47
48
	/**
49
	 * Initialize action listeners for full sync.
50
	 *
51
	 * @access public
52
	 *
53
	 * @param callable $callable Action handler callable.
54
	 */
55
	public function init_full_sync_listeners( $callable ) {
56
		// Synthetic actions for full sync.
57
		add_action( 'jetpack_full_sync_start', $callable, 10, 3 );
58
		add_action( 'jetpack_full_sync_end', $callable, 10, 2 );
59
		add_action( 'jetpack_full_sync_cancelled', $callable );
60
	}
61
62
	/**
63
	 * Initialize the module in the sender.
64
	 *
65
	 * @access public
66
	 */
67
	public function init_before_send() {
68
		// This is triggered after actions have been processed on the server.
69
		add_action( 'jetpack_sync_processed_actions', array( $this, 'update_sent_progress_action' ) );
70
	}
71
72
	/**
73
	 * Start a full sync.
74
	 *
75
	 * @access public
76
	 *
77
	 * @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...
78
	 * @return bool Always returns true at success.
79
	 */
80
	public function start( $module_configs = null ) {
81
		$was_already_running = $this->is_started() && ! $this->is_finished();
82
83
		// Remove all evidence of previous full sync items and status.
84
		$this->reset_data();
85
86
		if ( $was_already_running ) {
87
			/**
88
			 * Fires when a full sync is cancelled.
89
			 *
90
			 * @since 4.2.0
91
			 */
92
			do_action( 'jetpack_full_sync_cancelled' );
93
		}
94
95
		$this->update_status_option( 'started', time() );
96
		$this->update_status_option( 'params', $module_configs );
97
98
		$enqueue_status   = array();
99
		$full_sync_config = array();
100
		$include_empty    = false;
101
		$empty            = array();
102
103
		$full_sync_modules = array_filter(
104
			Modules::get_modules(),
105
			function( $module ) {
106
				return $module->is_full_sync_capable;
107
			}
108
		);
109
110
		// Default value is full sync.
111
		if ( ! is_array( $module_configs ) ) {
112
			$module_configs = array();
113
			$include_empty  = true;
114
			foreach ( $full_sync_modules as $module ) {
115
				$module_configs[ $module->name() ] = true;
116
			}
117
		}
118
119
		// Set default configuration, calculate totals, and save configuration if totals > 0.
120
		foreach ( $full_sync_modules as $module ) {
121
			$module_name   = $module->name();
122
			$module_config = isset( $module_configs[ $module_name ] ) ? $module_configs[ $module_name ] : false;
123
124
			if ( ! $module_config ) {
125
				continue;
126
			}
127
128
			if ( 'users' === $module_name && 'initial' === $module_config ) {
129
				$module_config = $module->get_initial_sync_user_config();
130
			}
131
132
			$enqueue_status[ $module_name ] = false;
133
134
			$total_items = $module->estimate_full_sync_actions( $module_config );
135
136
			// If there's information to process, configure this module.
137
			if ( ! is_null( $total_items ) && $total_items > 0 ) {
138
				$full_sync_config[ $module_name ] = $module_config;
139
				$enqueue_status[ $module_name ]   = array(
140
					$total_items,   // Total.
141
					0,              // Queued.
142
					false,          // Current state.
143
				);
144
			} elseif ( $include_empty && 0 === $total_items ) {
145
				$empty[ $module_name ] = true;
146
			}
147
		}
148
149
		$this->set_config( $full_sync_config );
150
		l( 'INITAL ENEQUE STATUS', $enqueue_status );
151
152
		$this->set_enqueue_status( $enqueue_status );
153
154
		$range = $this->get_content_range( $full_sync_config );
155
		/**
156
		 * Fires when a full sync begins. This action is serialized
157
		 * and sent to the server so that it knows a full sync is coming.
158
		 *
159
		 * @since 4.2.0
160
		 * @since 7.3.0 Added $range arg.
161
		 * @since 7.4.0 Added $empty arg.
162
		 *
163
		 * @param array $full_sync_config Sync configuration for all sync modules.
164
		 * @param array $range            Range of the sync items, containing min and max IDs for some item types.
165
		 * @param array $empty            The modules with no items to sync during a full sync.
166
		 */
167
		do_action( 'jetpack_full_sync_start', $full_sync_config, $range, $empty );
168
		l( 'do_action( \'jetpack_full_sync_start\'' );
169
170
		$this->continue_enqueuing( $full_sync_config, $enqueue_status );
171
172
		return true;
173
	}
174
175
	/**
176
	 * Enqueue the next items to sync.
177
	 *
178
	 * @access public
179
	 *
180
	 * @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...
181
	 * @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...
182
	 */
183
	public function continue_enqueuing( $configs = null, $enqueue_status = null ) {
184
		l( 'continue enqueuing' );
185
		if ( ! $this->is_started() || $this->get_status_option( 'queue_finished' ) ) {
186
			return;
187
		}
188
189
		// If full sync queue is full, don't enqueue more items.
190
		$max_queue_size_full_sync = Settings::get_setting( 'max_queue_size_full_sync' );
191
		$full_sync_queue          = new Queue( 'full_sync' );
192
193
		$available_queue_slots = $max_queue_size_full_sync - $full_sync_queue->size();
194
195
		l( 'FIRST STATUS' );
196
		l( $this->get_status() );
197
198
		if ( $available_queue_slots <= 0 ) {
199
			return;
200
		} else {
201
			$remaining_items_to_enqueue = min( Settings::get_setting( 'max_enqueue_full_sync' ), $available_queue_slots );
202
		}
203
204
		if ( ! $configs ) {
205
			$configs = $this->get_config();
206
		}
207
208
		if ( ! $enqueue_status ) {
209
			$enqueue_status = $this->get_enqueue_status();
210
		}
211
212
		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...
213
			$module_name = $module->name();
214
215
			// Skip module if not configured for this sync or module is done.
216
			if ( ! isset( $configs[ $module_name ] )
217
				|| // No module config.
218
					! $configs[ $module_name ]
219
				|| // No enqueue status.
220
					! $enqueue_status[ $module_name ]
221
				|| // Finished enqueuing this module.
222
					true === $enqueue_status[ $module_name ][2] ) {
223
				continue;
224
			}
225
226
			l( 'enqueue for module', $module_name );
227
			l( 'enqueue_status', $enqueue_status );
228
			list( $items_enqueued, $next_enqueue_state ) = $module->enqueue_full_sync_actions( $configs[ $module_name ], $remaining_items_to_enqueue, $enqueue_status[ $module_name ][2] );
229
230
			l( $items_enqueued, $next_enqueue_state );
231
			$enqueue_status[ $module_name ][2] = $next_enqueue_state;
232
233
			// If items were processed, subtract them from the limit.
234
			if ( ! is_null( $items_enqueued ) && $items_enqueued > 0 ) {
235
				$enqueue_status[ $module_name ][1] += $items_enqueued;
236
				$remaining_items_to_enqueue        -= $items_enqueued;
237
			}
238
239
			l( 'ENQUEUE STATUS', $enqueue_status );
240
			l( $remaining_items_to_enqueue );
241
			// Stop processing if we've reached our limit of items to enqueue.
242
			if ( 0 >= $remaining_items_to_enqueue ) {
243
244
				$this->set_enqueue_status( $enqueue_status );
245
				return;
246
			}
247
		}
248
249
		$this->set_enqueue_status( $enqueue_status );
250
251
		// Setting autoload to true means that it's faster to check whether we should continue enqueuing.
252
		$this->update_status_option( 'queue_finished', time(), true );
253
254
		$range = $this->get_content_range( $configs );
255
256
		/**
257
		 * Fires when a full sync ends. This action is serialized
258
		 * and sent to the server.
259
		 *
260
		 * @since 4.2.0
261
		 * @since 7.3.0 Added $range arg.
262
		 *
263
		 * @param string $checksum Deprecated since 7.3.0 - @see https://github.com/Automattic/jetpack/pull/11945/
264
		 * @param array  $range    Range of the sync items, containing min and max IDs for some item types.
265
		 */
266
		do_action( 'jetpack_full_sync_end', '', $range );
267
	}
268
269
	/**
270
	 * Get the range (min ID, max ID and total items) of items to sync.
271
	 *
272
	 * @access public
273
	 *
274
	 * @param string $type Type of sync item to get the range for.
275
	 * @return array Array of min ID, max ID and total items in the range.
276
	 */
277
	public function get_range( $type ) {
278
		global $wpdb;
279
		if ( ! in_array( $type, array( 'comments', 'posts' ), true ) ) {
280
			return array();
281
		}
282
283
		switch ( $type ) {
284
			case 'posts':
285
				$table     = $wpdb->posts;
286
				$id        = 'ID';
287
				$where_sql = Settings::get_blacklisted_post_types_sql();
288
289
				break;
290
			case 'comments':
291
				$table     = $wpdb->comments;
292
				$id        = 'comment_ID';
293
				$where_sql = Settings::get_comments_filter_sql();
294
				break;
295
		}
296
297
		// TODO: Call $wpdb->prepare on the following query.
298
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
299
		$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...
300
		if ( isset( $results[0] ) ) {
301
			return $results[0];
302
		}
303
304
		return array();
305
	}
306
307
	/**
308
	 * Get the range for content (posts and comments) to sync.
309
	 *
310
	 * @access private
311
	 *
312
	 * @param array $config Full sync configuration for this all sync modules.
313
	 * @return array Array of range (min ID, max ID, total items) for all content types.
314
	 */
315
	private function get_content_range( $config ) {
316
		$range = array();
317
		// Only when we are sending the whole range do we want to send also the range.
318 View Code Duplication
		if ( true === isset( $config['posts'] ) && $config['posts'] ) {
319
			$range['posts'] = $this->get_range( 'posts' );
320
		}
321
322 View Code Duplication
		if ( true === isset( $config['comments'] ) && $config['comments'] ) {
323
			$range['comments'] = $this->get_range( 'comments' );
324
		}
325
		return $range;
326
	}
327
328
	/**
329
	 * Update the progress after sync modules actions have been processed on the server.
330
	 *
331
	 * @access public
332
	 *
333
	 * @param array $actions Actions that have been processed on the server.
334
	 */
335
	public function update_sent_progress_action( $actions ) {
336
		// Quick way to map to first items with an array of arrays.
337
		$actions_with_counts = array_count_values( array_filter( array_map( array( $this, 'get_action_name' ), $actions ) ) );
338
339
		// Total item counts for each action.
340
		$actions_with_total_counts = $this->get_actions_totals( $actions );
341
342
		if ( ! $this->is_started() || $this->is_finished() ) {
343
			return;
344
		}
345
346
		if ( isset( $actions_with_counts['jetpack_full_sync_start'] ) ) {
347
			$this->update_status_option( 'send_started', time() );
348
		}
349
350
		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...
351
			$module_actions     = $module->get_full_sync_actions();
352
			$status_option_name = "{$module->name()}_sent";
353
			$total_option_name  = "{$status_option_name}_total";
354
			$items_sent         = $this->get_status_option( $status_option_name, 0 );
355
			$items_sent_total   = $this->get_status_option( $total_option_name, 0 );
356
357
			foreach ( $module_actions as $module_action ) {
358
				if ( isset( $actions_with_counts[ $module_action ] ) ) {
359
					$items_sent += $actions_with_counts[ $module_action ];
360
				}
361
362
				if ( ! empty( $actions_with_total_counts[ $module_action ] ) ) {
363
					$items_sent_total += $actions_with_total_counts[ $module_action ];
364
				}
365
			}
366
367
			if ( $items_sent > 0 ) {
368
				$this->update_status_option( $status_option_name, $items_sent );
369
			}
370
371
			if ( 0 !== $items_sent_total ) {
372
				$this->update_status_option( $total_option_name, $items_sent_total );
373
			}
374
		}
375
376
		if ( isset( $actions_with_counts['jetpack_full_sync_end'] ) ) {
377
			$this->update_status_option( 'finished', time() );
378
		}
379
	}
380
381
	/**
382
	 * Get the name of the action for an item in the sync queue.
383
	 *
384
	 * @access public
385
	 *
386
	 * @param array $queue_item Item of the sync queue.
387
	 * @return string|boolean Name of the action, false if queue item is invalid.
388
	 */
389
	public function get_action_name( $queue_item ) {
390
		if ( is_array( $queue_item ) && isset( $queue_item[0] ) ) {
391
			return $queue_item[0];
392
		}
393
		return false;
394
	}
395
396
	/**
397
	 * Retrieve the total number of items we're syncing in a particular queue item (action).
398
	 * `$queue_item[1]` is expected to contain chunks of items, and `$queue_item[1][0]`
399
	 * represents the first (and only) chunk of items to sync in that action.
400
	 *
401
	 * @access public
402
	 *
403
	 * @param array $queue_item Item of the sync queue that corresponds to a particular action.
404
	 * @return int Total number of items in the action.
405
	 */
406
	public function get_action_totals( $queue_item ) {
407
		if ( is_array( $queue_item ) && isset( $queue_item[1][0] ) ) {
408
			if ( is_array( $queue_item[1][0] ) ) {
409
				// Let's count the items we sync in this action.
410
				return count( $queue_item[1][0] );
411
			}
412
			// -1 indicates that this action syncs all items by design.
413
			return -1;
414
		}
415
		return 0;
416
	}
417
418
	/**
419
	 * Retrieve the total number of items for a set of actions, grouped by action name.
420
	 *
421
	 * @access public
422
	 *
423
	 * @param array $actions An array of actions.
424
	 * @return array An array, representing the total number of items, grouped per action.
425
	 */
426
	public function get_actions_totals( $actions ) {
427
		$totals = array();
428
429
		foreach ( $actions as $action ) {
430
			$name          = $this->get_action_name( $action );
431
			$action_totals = $this->get_action_totals( $action );
432
			if ( ! isset( $totals[ $name ] ) ) {
433
				$totals[ $name ] = 0;
434
			}
435
			$totals[ $name ] += $action_totals;
436
		}
437
438
		return $totals;
439
	}
440
441
	/**
442
	 * Whether full sync has started.
443
	 *
444
	 * @access public
445
	 *
446
	 * @return boolean
447
	 */
448
	public function is_started() {
449
		return ! ! $this->get_status_option( 'started' );
450
	}
451
452
	/**
453
	 * Whether full sync has finished.
454
	 *
455
	 * @access public
456
	 *
457
	 * @return boolean
458
	 */
459
	public function is_finished() {
460
		return ! ! $this->get_status_option( 'finished' );
461
	}
462
463
	/**
464
	 * Retrieve the status of the current full sync.
465
	 *
466
	 * @access public
467
	 *
468
	 * @return array Full sync status.
469
	 */
470
	public function get_status() {
471
		$status = array(
472
			'started'        => $this->get_status_option( 'started' ),
473
			'queue_finished' => $this->get_status_option( 'queue_finished' ),
474
			'send_started'   => $this->get_status_option( 'send_started' ),
475
			'finished'       => $this->get_status_option( 'finished' ),
476
			'sent'           => array(),
477
			'sent_total'     => array(),
478
			'queue'          => array(),
479
			'config'         => $this->get_status_option( 'params' ),
480
			'total'          => array(),
481
		);
482
483
		$enqueue_status = $this->get_enqueue_status();
484
485
		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...
486
			$name = $module->name();
487
488
			if ( ! isset( $enqueue_status[ $name ] ) ) {
489
				continue;
490
			}
491
492
			list( $total, $queued ) = $enqueue_status[ $name ];
493
494
			if ( $total ) {
495
				$status['total'][ $name ] = $total;
496
			}
497
498
			if ( $queued ) {
499
				$status['queue'][ $name ] = $queued;
500
			}
501
502
			$sent = $this->get_status_option( "{$name}_sent" );
503
			if ( $sent ) {
504
				$status['sent'][ $name ] = $sent;
505
			}
506
507
			$sent_total = $this->get_status_option( "{$name}_sent_total" );
508
			if ( $sent_total ) {
509
				$status['sent_total'][ $name ] = $sent_total;
510
			}
511
		}
512
513
		return $status;
514
	}
515
516
	/**
517
	 * Clear all the full sync status options.
518
	 *
519
	 * @access public
520
	 */
521
	public function clear_status() {
522
		$prefix = self::STATUS_OPTION_PREFIX;
523
		\Jetpack_Options::delete_raw_option( "{$prefix}_started" );
524
		\Jetpack_Options::delete_raw_option( "{$prefix}_params" );
525
		\Jetpack_Options::delete_raw_option( "{$prefix}_queue_finished" );
526
		\Jetpack_Options::delete_raw_option( "{$prefix}_send_started" );
527
		\Jetpack_Options::delete_raw_option( "{$prefix}_enqueue_status" );
528
		\Jetpack_Options::delete_raw_option( "{$prefix}_finished" );
529
530
		$this->delete_enqueue_status();
531
532
		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...
533
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent" );
534
			\Jetpack_Options::delete_raw_option( "{$prefix}_{$module->name()}_sent_total" );
535
		}
536
	}
537
538
	/**
539
	 * Clear all the full sync data.
540
	 *
541
	 * @access public
542
	 */
543
	public function reset_data() {
544
		$this->clear_status();
545
		$this->delete_config();
546
547
		$listener = Listener::get_instance();
548
		$listener->get_full_sync_queue()->reset();
549
	}
550
551
	/**
552
	 * Get the value of a full sync status option.
553
	 *
554
	 * @access private
555
	 *
556
	 * @param string $name    Name of the option.
557
	 * @param mixed  $default Default value of the option.
558
	 * @return mixed Option value.
559
	 */
560
	private function get_status_option( $name, $default = null ) {
561
		$value = \Jetpack_Options::get_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $default );
562
563
		return is_numeric( $value ) ? intval( $value ) : $value;
564
	}
565
566
	/**
567
	 * Update the value of a full sync status option.
568
	 *
569
	 * @access private
570
	 *
571
	 * @param string  $name     Name of the option.
572
	 * @param mixed   $value    Value of the option.
573
	 * @param boolean $autoload Whether the option should be autoloaded at the beginning of the request.
574
	 */
575
	private function update_status_option( $name, $value, $autoload = false ) {
576
		\Jetpack_Options::update_raw_option( self::STATUS_OPTION_PREFIX . "_$name", $value, $autoload );
577
	}
578
579
	/**
580
	 * Set the full sync enqueue status.
581
	 *
582
	 * @access private
583
	 *
584
	 * @param array $new_status The new full sync enqueue status.
585
	 */
586
	private function set_enqueue_status( $new_status ) {
587
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_enqueue_status', $new_status );
588
	}
589
590
	/**
591
	 * Delete full sync enqueue status.
592
	 *
593
	 * @access private
594
	 *
595
	 * @return boolean Whether the status was deleted.
596
	 */
597
	private function delete_enqueue_status() {
598
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_enqueue_status' );
599
	}
600
601
	/**
602
	 * Retrieve the current full sync enqueue status.
603
	 *
604
	 * @access private
605
	 *
606
	 * @return array Full sync enqueue status.
607
	 */
608
	private function get_enqueue_status() {
609
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_enqueue_status' );
610
	}
611
612
	/**
613
	 * Set the full sync enqueue configuration.
614
	 *
615
	 * @access private
616
	 *
617
	 * @param array $config The new full sync enqueue configuration.
618
	 */
619
	private function set_config( $config ) {
620
		\Jetpack_Options::update_raw_option( 'jetpack_sync_full_config', $config );
621
	}
622
623
	/**
624
	 * Delete full sync configuration.
625
	 *
626
	 * @access private
627
	 *
628
	 * @return boolean Whether the configuration was deleted.
629
	 */
630
	private function delete_config() {
631
		return \Jetpack_Options::delete_raw_option( 'jetpack_sync_full_config' );
632
	}
633
634
	/**
635
	 * Retrieve the current full sync enqueue config.
636
	 *
637
	 * @access private
638
	 *
639
	 * @return array Full sync enqueue config.
640
	 */
641
	private function get_config() {
642
		return \Jetpack_Options::get_raw_option( 'jetpack_sync_full_config' );
643
	}
644
645
	/**
646
	 * Update an option manually to bypass filters and caching.
647
	 *
648
	 * @access private
649
	 *
650
	 * @param string $name  Option name.
651
	 * @param mixed  $value Option value.
652
	 * @return int The number of updated rows in the database.
653
	 */
654
	private function write_option( $name, $value ) {
655
		// We write our own option updating code to bypass filters/caching/etc on set_option/get_option.
656
		global $wpdb;
657
		$serialized_value = maybe_serialize( $value );
658
659
		/**
660
		 * Try updating, if no update then insert
661
		 * TODO: try to deal with the fact that unchanged values can return updated_num = 0
662
		 * below we used "insert ignore" to at least suppress the resulting error.
663
		 */
664
		$updated_num = $wpdb->query(
665
			$wpdb->prepare(
666
				"UPDATE $wpdb->options SET option_value = %s WHERE option_name = %s",
667
				$serialized_value,
668
				$name
669
			)
670
		);
671
672
		if ( ! $updated_num ) {
673
			$updated_num = $wpdb->query(
674
				$wpdb->prepare(
675
					"INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, 'no' )",
676
					$name,
677
					$serialized_value
678
				)
679
			);
680
		}
681
		return $updated_num;
682
	}
683
684
	/**
685
	 * Update an option manually to bypass filters and caching.
686
	 *
687
	 * @access private
688
	 *
689
	 * @param string $name    Option name.
690
	 * @param mixed  $default Default option value.
691
	 * @return mixed Option value.
692
	 */
693
	private function read_option( $name, $default = null ) {
694
		global $wpdb;
695
		$value = $wpdb->get_var(
696
			$wpdb->prepare(
697
				"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
698
				$name
699
			)
700
		);
701
		$value = maybe_unserialize( $value );
702
703
		if ( null === $value && null !== $default ) {
704
			return $default;
705
		}
706
707
		return $value;
708
	}
709
}
710