Completed
Push — fix/amp-wpcom-toolbar ( b081a1...85d265 )
by
unknown
55:18 queued 43:30
created

Jetpack_CLI   F

Complexity

Total Complexity 289

Size/Duplication

Total Lines 2037
Duplicated Lines 6.14 %

Coupling/Cohesion

Components 2
Dependencies 17

Importance

Changes 0
Metric Value
dl 125
loc 2037
rs 0.8
c 0
b 0
f 0
wmc 289
lcom 2
cbo 17

23 Methods

Rating   Name   Duplication   Size   Complexity  
C status() 4 79 12
A test_connection() 0 34 5
D disconnect() 4 72 16
F reset() 39 155 23
A count_option() 0 10 1
F module() 5 74 20
F protect() 4 105 20
F options() 7 96 25
F sync() 47 194 41
B sync_queue() 0 50 8
C partner_cancel() 6 56 10
B partner_provision() 3 38 7
A sitemap() 0 19 6
A authorize_user() 0 34 4
C call_api() 0 70 12
B upload_ssh_creds() 0 53 8
A get_stats() 0 35 3
F publicize() 0 176 41
A get_api_host() 0 4 2
A partner_provision_error() 0 12 1
A scaffold() 0 12 2
F block() 6 168 21
A render_block_file() 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 Jetpack_CLI 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 Jetpack_CLI, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use Automattic\Jetpack\Connection\Client;
4
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
5
use Automattic\Jetpack\Connection\Tokens;
6
use Automattic\Jetpack\Status;
7
use Automattic\Jetpack\Sync\Actions;
8
use Automattic\Jetpack\Sync\Listener;
9
use Automattic\Jetpack\Sync\Modules;
10
use Automattic\Jetpack\Sync\Queue;
11
use Automattic\Jetpack\Sync\Settings;
12
13
WP_CLI::add_command( 'jetpack', 'Jetpack_CLI' );
14
15
/**
16
 * Control your local Jetpack installation.
17
 */
18
class Jetpack_CLI extends WP_CLI_Command {
19
	// Aesthetics.
20
	public $green_open  = "\033[32m";
21
	public $red_open    = "\033[31m";
22
	public $yellow_open = "\033[33m";
23
	public $color_close = "\033[0m";
24
25
	/**
26
	 * Get Jetpack Details
27
	 *
28
	 * ## OPTIONS
29
	 *
30
	 * empty: Leave it empty for basic stats
31
	 *
32
	 * full: View full stats.  It's the data from the heartbeat
33
	 *
34
	 * ## EXAMPLES
35
	 *
36
	 * wp jetpack status
37
	 * wp jetpack status full
38
	 */
39
	public function status( $args, $assoc_args ) {
40
		jetpack_require_lib( 'debugger' );
41
42
		/* translators: %s is the site URL */
43
		WP_CLI::line( sprintf( __( 'Checking status for %s', 'jetpack' ), esc_url( get_home_url() ) ) );
44
45 View Code Duplication
		if ( isset( $args[0] ) && 'full' !== $args[0] ) {
46
			/* translators: %s is a command like "prompt" */
47
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $args[0] ) );
48
		}
49
50
		$master_user_email = Jetpack::get_master_user_email();
51
52
		$cxntests = new Jetpack_Cxn_Tests();
53
54
		if ( $cxntests->pass() ) {
55
			$cxntests->output_results_for_cli();
56
57
			WP_CLI::success( __( 'Jetpack is currently connected to WordPress.com', 'jetpack' ) );
58
		} else {
59
			$error = array();
60
			foreach ( $cxntests->list_fails() as $fail ) {
0 ignored issues
show
Bug introduced by
The expression $cxntests->list_fails() of type false|array 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...
61
				$error[] = $fail['name'] . ': ' . $fail['message'];
62
			}
63
			WP_CLI::error_multi_line( $error );
64
65
			$cxntests->output_results_for_cli();
66
67
			WP_CLI::error( __( 'One or more tests did not pass. Please investigate!', 'jetpack' ) ); // Exit CLI.
68
		}
69
70
		/* translators: %s is current version of Jetpack, for example 7.3 */
71
		WP_CLI::line( sprintf( __( 'The Jetpack Version is %s', 'jetpack' ), JETPACK__VERSION ) );
72
		/* translators: %d is WP.com ID of this blog */
73
		WP_CLI::line( sprintf( __( 'The WordPress.com blog_id is %d', 'jetpack' ), Jetpack_Options::get_option( 'id' ) ) );
74
		/* translators: %s is the email address of the connection owner */
75
		WP_CLI::line( sprintf( __( 'The WordPress.com account for the primary connection is %s', 'jetpack' ), $master_user_email ) );
76
77
		/*
78
		 * Are they asking for all data?
79
		 *
80
		 * Loop through heartbeat data and organize by priority.
81
		 */
82
		$all_data = ( isset( $args[0] ) && 'full' == $args[0] ) ? 'full' : false;
83
		if ( $all_data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $all_data of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
84
			// Heartbeat data
85
			WP_CLI::line( "\n" . __( 'Additional data: ', 'jetpack' ) );
86
87
			// Get the filtered heartbeat data.
88
			// Filtered so we can color/list by severity
89
			$stats = Jetpack::jetpack_check_heartbeat_data();
90
91
			// Display red flags first
92
			foreach ( $stats['bad'] as $stat => $value ) {
93
				printf( "$this->red_open%-'.16s %s $this->color_close\n", $stat, $value );
94
			}
95
96
			// Display caution warnings next
97
			foreach ( $stats['caution'] as $stat => $value ) {
98
				printf( "$this->yellow_open%-'.16s %s $this->color_close\n", $stat, $value );
99
			}
100
101
			// The rest of the results are good!
102
			foreach ( $stats['good'] as $stat => $value ) {
103
104
				// Modules should get special spacing for aestetics
105
				if ( strpos( $stat, 'odule-' ) ) {
106
					printf( "%-'.30s %s\n", $stat, $value );
107
					usleep( 4000 ); // For dramatic effect lolz
108
					continue;
109
				}
110
				printf( "%-'.16s %s\n", $stat, $value );
111
				usleep( 4000 ); // For dramatic effect lolz
112
			}
113
		} else {
114
			// Just the basics
115
			WP_CLI::line( "\n" . _x( "View full status with 'wp jetpack status full'", '"wp jetpack status full" is a command - do not translate', 'jetpack' ) );
116
		}
117
	}
118
119
	/**
120
	 * Tests the active connection
121
	 *
122
	 * Does a two-way test to verify that the local site can communicate with remote Jetpack/WP.com servers and that Jetpack/WP.com servers can talk to the local site.
123
	 *
124
	 * ## EXAMPLES
125
	 *
126
	 * wp jetpack test-connection
127
	 *
128
	 * @subcommand test-connection
129
	 */
130
	public function test_connection( $args, $assoc_args ) {
131
132
		/* translators: %s is the site URL */
133
		WP_CLI::line( sprintf( __( 'Testing connection for %s', 'jetpack' ), esc_url( get_site_url() ) ) );
134
135
		if ( ! Jetpack::is_connection_ready() ) {
136
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
137
		}
138
139
		$response = Client::wpcom_json_api_request_as_blog(
140
			sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
141
			Client::WPCOM_JSON_API_VERSION
142
		);
143
144
		if ( is_wp_error( $response ) ) {
145
			/* translators: %1$s is the error code, %2$s is the error message */
146
			WP_CLI::error( sprintf( __( 'Failed to test connection (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() ) );
147
		}
148
149
		$body = wp_remote_retrieve_body( $response );
150
		if ( ! $body ) {
151
			WP_CLI::error( __( 'Failed to test connection (empty response body)', 'jetpack' ) );
152
		}
153
154
		$result       = json_decode( $body );
155
		$is_connected = (bool) $result->connected;
156
		$message      = $result->message;
157
158
		if ( $is_connected ) {
159
			WP_CLI::success( $message );
160
		} else {
161
			WP_CLI::error( $message );
162
		}
163
	}
164
165
	/**
166
	 * Disconnect Jetpack Blogs or Users
167
	 *
168
	 * ## OPTIONS
169
	 *
170
	 * blog: Disconnect the entire blog.
171
	 *
172
	 * user <user_identifier>: Disconnect a specific user from WordPress.com.
173
	 *
174
	 * [--force]
175
	 * If the user ID provided is the connection owner, it will only be disconnected if --force is passed
176
	 *
177
	 * ## EXAMPLES
178
	 *
179
	 * wp jetpack disconnect blog
180
	 * wp jetpack disconnect user 13
181
	 * wp jetpack disconnect user 1 --force
182
	 * wp jetpack disconnect user username
183
	 * wp jetpack disconnect user [email protected]
184
	 *
185
	 * @synopsis <blog|user> [<user_identifier>] [--force]
186
	 */
187
	public function disconnect( $args, $assoc_args ) {
188
		if ( ! Jetpack::is_connection_ready() ) {
189
			WP_CLI::success( __( 'The site is not currently connected, so nothing to do!', 'jetpack' ) );
190
			return;
191
		}
192
193
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
194 View Code Duplication
		if ( ! in_array( $action, array( 'blog', 'user', 'prompt' ) ) ) {
195
			/* translators: %s is a command like "prompt" */
196
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
197
		}
198
199
		if ( in_array( $action, array( 'user' ) ) ) {
200
			if ( isset( $args[1] ) ) {
201
				$user_id = $args[1];
202
				if ( ctype_digit( $user_id ) ) {
203
					$field   = 'id';
204
					$user_id = (int) $user_id;
205
				} elseif ( is_email( $user_id ) ) {
206
					$field   = 'email';
207
					$user_id = sanitize_user( $user_id, true );
208
				} else {
209
					$field   = 'login';
210
					$user_id = sanitize_user( $user_id, true );
211
				}
212
				if ( ! $user = get_user_by( $field, $user_id ) ) {
213
					WP_CLI::error( __( 'Please specify a valid user.', 'jetpack' ) );
214
				}
215
			} else {
216
				WP_CLI::error( __( 'Please specify a user by either ID, username, or email.', 'jetpack' ) );
217
			}
218
		}
219
220
		$force_user_disconnect = ! empty( $assoc_args['force'] );
221
222
		switch ( $action ) {
223
			case 'blog':
224
				Jetpack::log( 'disconnect' );
225
				Jetpack::disconnect();
226
				WP_CLI::success(
227
					sprintf(
228
						/* translators: %s is the site URL */
229
						__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
230
						esc_url( get_site_url() )
231
					)
232
				);
233
				break;
234
			case 'user':
235
				$connection_manager = new Connection_Manager( 'jetpack' );
236
				$disconnected       = $connection_manager->disconnect_user( $user->ID, $force_user_disconnect );
0 ignored issues
show
Bug introduced by
The variable $user 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...
237
				if ( $disconnected ) {
238
					Jetpack::log( 'unlink', $user->ID );
239
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
240
				} else {
241
					if ( ! $connection_manager->is_user_connected( $user->ID ) ) {
242
						/* translators: %s is a username */
243
						$error_message = sprintf( __( 'User %s could not be disconnected because it is not connected!', 'jetpack' ), "{$user->data->user_login} <{$user->data->user_email}>" );
244
					} elseif ( ! $force_user_disconnect && $connection_manager->is_connection_owner( $user->ID ) ) {
245
						/* translators: %s is a username */
246
						$error_message = sprintf( __( 'User %s could not be disconnected because it is the connection owner! If you want to disconnect in anyway, use the --force parameter.', 'jetpack' ), "{$user->data->user_login} <{$user->data->user_email}>" );
247
					} else {
248
						/* translators: %s is a username */
249
						$error_message = sprintf( __( 'User %s could not be disconnected.', 'jetpack' ), "{$user->data->user_login} <{$user->data->user_email}>" );
250
					}
251
					WP_CLI::error( $error_message );
252
				}
253
				break;
254
			case 'prompt':
255
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
256
				break;
257
		}
258
	}
259
260
	/**
261
	 * Reset Jetpack options and settings to default
262
	 *
263
	 * ## OPTIONS
264
	 *
265
	 * modules: Resets modules to default state ( get_default_modules() )
266
	 *
267
	 * options: Resets all Jetpack options except:
268
	 *  - All private options (Blog token, user token, etc...)
269
	 *  - id (The Client ID/WP.com Blog ID of this site)
270
	 *  - master_user
271
	 *  - version
272
	 *  - activated
273
	 *
274
	 * ## EXAMPLES
275
	 *
276
	 * wp jetpack reset options
277
	 * wp jetpack reset modules
278
	 * wp jetpack reset sync-checksum --dry-run --offset=0
279
	 *
280
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
281
	 */
282
	public function reset( $args, $assoc_args ) {
283
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
284 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
285
			/* translators: %s is a command like "prompt" */
286
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
287
		}
288
289
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
290
291 View Code Duplication
		if ( $is_dry_run ) {
292
			WP_CLI::warning(
293
				__( "\nThis is a dry run.\n", 'jetpack' ) .
294
				__( "No actions will be taken.\n", 'jetpack' ) .
295
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
296
			);
297
		} else {
298
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
299
			jetpack_cli_are_you_sure();
300
		}
301
302
		switch ( $action ) {
303
			case 'options':
304
				$options_to_reset = Jetpack_Options::get_options_for_reset();
305
				// Reset the Jetpack options
306
				WP_CLI::line(
307
					sprintf(
308
						/* translators: %s is the site URL */
309
						__( "Resetting Jetpack Options for %s...\n", 'jetpack' ),
310
						esc_url( get_site_url() )
311
					)
312
				);
313
				sleep( 1 ); // Take a breath
314 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
315
					if ( ! $is_dry_run ) {
316
						Jetpack_Options::delete_option( $option_to_reset );
317
						usleep( 100000 );
318
					}
319
320
					/* translators: This is the result of an action. The option named %s was reset */
321
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
322
				}
323
324
				// Reset the WP options
325
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", 'jetpack' ) );
326
				usleep( 500000 ); // Take a breath
327 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
328
					if ( ! $is_dry_run ) {
329
						delete_option( $option_to_reset );
330
						usleep( 100000 );
331
					}
332
					/* translators: This is the result of an action. The option named %s was reset */
333
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
334
				}
335
336
				// Reset to default modules
337
				WP_CLI::line( __( "Resetting default modules...\n", 'jetpack' ) );
338
				usleep( 500000 ); // Take a breath
339
				$default_modules = Jetpack::get_default_modules();
340
				if ( ! $is_dry_run ) {
341
					Jetpack::update_active_modules( $default_modules );
342
				}
343
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
344
				break;
345 View Code Duplication
			case 'modules':
346
				if ( ! $is_dry_run ) {
347
					$default_modules = Jetpack::get_default_modules();
348
					Jetpack::update_active_modules( $default_modules );
349
				}
350
351
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
352
				break;
353
			case 'prompt':
354
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
355
				break;
356
			case 'sync-checksum':
357
				$option = 'jetpack_callables_sync_checksum';
358
359
				if ( is_multisite() ) {
360
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
361
362
					/*
363
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
364
					 * Offset can be used to paginate and try to clean up more sites.
365
					 */
366
					$sites       = get_sites(
367
						array(
368
							'number' => 1000,
369
							'offset' => $offset,
370
						)
371
					);
372
					$count_fixes = 0;
373
					foreach ( $sites as $site ) {
374
						switch_to_blog( $site->blog_id );
375
						$count = self::count_option( $option );
376
						if ( $count > 1 ) {
377
							if ( ! $is_dry_run ) {
378
								delete_option( $option );
379
							}
380
							WP_CLI::line(
381
								sprintf(
382
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
383
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
384
									$count,
385
									$option,
386
									"{$site->domain}{$site->path}"
387
								)
388
							);
389
							$count_fixes++;
390
							if ( ! $is_dry_run ) {
391
								/*
392
								 * We could be deleting a lot of options rows at the same time.
393
								 * Allow some time for replication to catch up.
394
								 */
395
								sleep( 3 );
396
							}
397
						}
398
399
						restore_current_blog();
400
					}
401
					if ( $count_fixes ) {
402
						WP_CLI::success(
403
							sprintf(
404
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
405
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
406
								$option,
407
								$count_fixes
408
							)
409
						);
410
					} else {
411
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
412
					}
413
					return;
414
				}
415
416
				$count = self::count_option( $option );
417
				if ( $count > 1 ) {
418
					if ( ! $is_dry_run ) {
419
						delete_option( $option );
420
					}
421
					WP_CLI::success(
422
						sprintf(
423
							/* translators: %1$d is a number, %2$s is the name of an option. */
424
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
425
							$count,
426
							$option
427
						)
428
					);
429
					return;
430
				}
431
432
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
433
				break;
434
435
		}
436
	}
437
438
	/**
439
	 * Return the number of times an option appears
440
	 * Normally an option would only appear 1 since the option key is supposed to be unique
441
	 * but if a site hasn't updated the DB schema then that would not be the case.
442
	 *
443
	 * @param string $option Option name.
444
	 *
445
	 * @return int
446
	 */
447
	private static function count_option( $option ) {
448
		global $wpdb;
449
		return (int) $wpdb->get_var(
450
			$wpdb->prepare(
451
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
452
				$option
453
			)
454
		);
455
456
	}
457
458
	/**
459
	 * Manage Jetpack Modules
460
	 *
461
	 * ## OPTIONS
462
	 *
463
	 * <list|activate|deactivate|toggle>
464
	 * : The action to take.
465
	 * ---
466
	 * default: list
467
	 * options:
468
	 *  - list
469
	 *  - activate
470
	 *  - deactivate
471
	 *  - toggle
472
	 * ---
473
	 *
474
	 * [<module_slug>]
475
	 * : The slug of the module to perform an action on.
476
	 *
477
	 * [--format=<format>]
478
	 * : Allows overriding the output of the command when listing modules.
479
	 * ---
480
	 * default: table
481
	 * options:
482
	 *  - table
483
	 *  - json
484
	 *  - csv
485
	 *  - yaml
486
	 *  - ids
487
	 *  - count
488
	 * ---
489
	 *
490
	 * ## EXAMPLES
491
	 *
492
	 * wp jetpack module list
493
	 * wp jetpack module list --format=json
494
	 * wp jetpack module activate stats
495
	 * wp jetpack module deactivate stats
496
	 * wp jetpack module toggle stats
497
	 * wp jetpack module activate all
498
	 * wp jetpack module deactivate all
499
	 */
500
	public function module( $args, $assoc_args ) {
501
		$action = isset( $args[0] ) ? $args[0] : 'list';
502
503
		if ( isset( $args[1] ) ) {
504
			$module_slug = $args[1];
505
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
506
				/* translators: %s is a module slug like "stats" */
507
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
508
			}
509
			if ( 'toggle' === $action ) {
510
				$action = Jetpack::is_module_active( $module_slug )
511
					? 'deactivate'
512
					: 'activate';
513
			}
514
			if ( 'all' === $args[1] ) {
515
				$action = ( 'deactivate' === $action )
516
					? 'deactivate_all'
517
					: 'activate_all';
518
			}
519
		} elseif ( 'list' !== $action ) {
520
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
521
			$action = 'list';
522
		}
523
524
		switch ( $action ) {
525
			case 'list':
526
				$modules_list = array();
527
				$modules      = Jetpack::get_available_modules();
528
				sort( $modules );
529
				foreach ( (array) $modules as $module_slug ) {
530
					if ( 'vaultpress' === $module_slug ) {
531
						continue;
532
					}
533
					$modules_list[] = array(
534
						'slug'   => $module_slug,
535
						'status' => Jetpack::is_module_active( $module_slug )
536
							? __( 'Active', 'jetpack' )
537
							: __( 'Inactive', 'jetpack' ),
538
					);
539
				}
540
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
541
				break;
542
			case 'activate':
543
				$module = Jetpack::get_module( $module_slug );
0 ignored issues
show
Bug introduced by
The variable $module_slug 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...
544
				Jetpack::log( 'activate', $module_slug );
545
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
546
					/* translators: %s is the name of a Jetpack module */
547
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
548
				} else {
549
					/* translators: %s is the name of a Jetpack module */
550
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
551
				}
552
				break;
553 View Code Duplication
			case 'activate_all':
554
				$modules = Jetpack::get_available_modules();
555
				Jetpack::update_active_modules( $modules );
556
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
557
				break;
558
			case 'deactivate':
559
				$module = Jetpack::get_module( $module_slug );
560
				Jetpack::log( 'deactivate', $module_slug );
561
				Jetpack::deactivate_module( $module_slug );
562
				/* translators: %s is the name of a Jetpack module */
563
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
564
				break;
565
			case 'deactivate_all':
566
				Jetpack::delete_active_modules();
567
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
568
				break;
569
			case 'toggle':
570
				// Will never happen, should have been handled above and changed to activate or deactivate.
571
				break;
572
		}
573
	}
574
575
	/**
576
	 * Manage Protect Settings
577
	 *
578
	 * ## OPTIONS
579
	 *
580
	 * allow: Add an IP address to an always allow list.  You can also read or clear the allow list.
581
	 *
582
	 *
583
	 * ## EXAMPLES
584
	 *
585
	 * wp jetpack protect allow <ip address>
586
	 * wp jetpack protect allow list
587
	 * wp jetpack protect allow clear
588
	 *
589
	 * @synopsis <allow> [<ip|ip_low-ip_high|list|clear>]
590
	 */
591
	public function protect( $args, $assoc_args ) {
592
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
593 View Code Duplication
		if ( ! in_array( $action, array( 'whitelist', 'allow' ), true ) ) { // Still allow "whitelist" for legacy support.
594
			/* translators: %s is a command like "prompt" */
595
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
596
		}
597
		// Check if module is active
598
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
599
			/* translators: %s is a module name */
600
			WP_CLI::error( sprintf( _x( '%1$s is not active. You can activate it with "wp jetpack module activate %2$s"', '"wp jetpack module activate" is a command - do not translate', 'jetpack' ), __FUNCTION__, __FUNCTION__ ) );
601
		}
602
		if ( in_array( $action, array( 'allow', 'whitelist' ), true ) ) {
603
			if ( isset( $args[1] ) ) {
604
				$action = 'allow';
605
			} else {
606
				$action = 'prompt';
607
			}
608
		}
609
		switch ( $action ) {
610
			case 'allow':
611
				$allow         = array();
612
				$new_ip        = $args[1];
613
				$current_allow = get_site_option( 'jetpack_protect_whitelist', array() ); // @todo Update the option name.
614
615
				// Build array of IPs that are already on the allowed list.
616
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
617
				// low & high range params for jetpack_protect_ip_address_is_in_range();
618
				foreach ( $current_allow as $allowed ) {
619
620
					// IP ranges
621
					if ( $allowed->range ) {
622
623
						// Is it already on the allowed list?
624
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $allowed->range_low, $allowed->range_high ) ) {
625
							/* translators: %s is an IP address */
626
							WP_CLI::error( sprintf( __( '%s is already on the always allow list.', 'jetpack' ), $new_ip ) );
627
							break;
628
						}
629
						$allow[] = $allowed->range_low . ' - ' . $allowed->range_high;
630
631
					} else { // Individual IPs
632
633
						// Check if the IP is already on the allow list (single IP only).
634
						if ( $new_ip === $allowed->ip_address ) {
635
							/* translators: %s is an IP address */
636
							WP_CLI::error( sprintf( __( '%s is already on the always allow list.', 'jetpack' ), $new_ip ) );
637
							break;
638
						}
639
						$allow[] = $allowed->ip_address;
640
641
					}
642
				}
643
644
				/*
645
				 * List the allowed IPs.
646
				 * Done here because it's easier to read the $allow array after it's been rebuilt.
647
				 */
648
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
649
					if ( ! empty( $allow ) ) {
650
						WP_CLI::success( __( 'Here are your always allowed IPs:', 'jetpack' ) );
651
						foreach ( $allow as $ip ) {
652
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) );
653
						}
654
					} else {
655
						WP_CLI::line( __( 'Always allow list is empty.', 'jetpack' ) );
656
					}
657
					break;
658
				}
659
660
				/*
661
				 * Clear the always allow list.
662
				 */
663
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
664
					if ( ! empty( $allow ) ) {
665
						$allow = array();
666
						jetpack_protect_save_whitelist( $allow ); // @todo Need to update function name in the Protect module.
667
						WP_CLI::success( __( 'Cleared all IPs from the always allow list.', 'jetpack' ) );
668
					} else {
669
						WP_CLI::line( __( 'Always allow list is empty.', 'jetpack' ) );
670
					}
671
					break;
672
				}
673
674
				// Append new IP to allow array.
675
				array_push( $allow, $new_ip );
676
677
				// Save allow list if there are no errors.
678
				$result = jetpack_protect_save_whitelist( $allow ); // @todo Need to update function name in the Protect module.
679
				if ( is_wp_error( $result ) ) {
680
					WP_CLI::error( $result );
681
				}
682
683
				/* translators: %s is an IP address */
684
				WP_CLI::success( sprintf( __( '%s has been added to the always allowed list.', 'jetpack' ), $new_ip ) );
685
				break;
686
			case 'prompt':
687
				WP_CLI::error(
688
					__( 'No command found.', 'jetpack' ) . "\n" .
689
					__( 'Please enter the IP address you want to always allow.', 'jetpack' ) . "\n" .
690
					_x( 'You can save a range of IPs {low_range}-{high_range}. No spaces allowed.  (example: 1.1.1.1-2.2.2.2)', 'Instructions on how to add IP ranges - low_range/high_range should be translated.', 'jetpack' ) . "\n" .
691
					_x( "You can also 'list' or 'clear' the always allowed list.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
692
				);
693
				break;
694
		}
695
	}
696
697
	/**
698
	 * Manage Jetpack Options
699
	 *
700
	 * ## OPTIONS
701
	 *
702
	 * list   : List all jetpack options and their values
703
	 * delete : Delete an option
704
	 *          - can only delete options that are white listed.
705
	 * update : update an option
706
	 *          - can only update option strings
707
	 * get    : get the value of an option
708
	 *
709
	 * ## EXAMPLES
710
	 *
711
	 * wp jetpack options list
712
	 * wp jetpack options get    <option_name>
713
	 * wp jetpack options delete <option_name>
714
	 * wp jetpack options update <option_name> [<option_value>]
715
	 *
716
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
717
	 */
718
	public function options( $args, $assoc_args ) {
719
		$action         = isset( $args[0] ) ? $args[0] : 'list';
720
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
721
722
		// Is the option flagged as unsafe?
723
		$flagged = ! in_array( $args[1], $safe_to_modify );
724
725 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
726
			/* translators: %s is a command like "prompt" */
727
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
728
		}
729
730
		if ( isset( $args[0] ) ) {
731
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
732
				$action = 'get';
733
			} elseif ( 'delete' == $args[0] && isset( $args[1] ) ) {
734
				$action = 'delete';
735
			} elseif ( 'update' == $args[0] && isset( $args[1] ) ) {
736
				$action = 'update';
737
			} else {
738
				$action = 'list';
739
			}
740
		}
741
742
		// Bail if the option isn't found
743
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
744 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
745
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
746
		}
747
748
		// Let's print_r the option if it's an array
749
		// Used in the 'get' and 'list' actions
750
		$option = is_array( $option ) ? print_r( $option ) : $option;
751
752
		switch ( $action ) {
753
			case 'get':
754
				WP_CLI::success( "\t" . $option );
755
				break;
756
			case 'delete':
757
				jetpack_cli_are_you_sure( $flagged );
758
759
				Jetpack_Options::delete_option( $args[1] );
760
				/* translators: %s is the option name */
761
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
762
				break;
763
			case 'update':
764
				jetpack_cli_are_you_sure( $flagged );
765
766
				// Updating arrays would get pretty tricky...
767
				$value = Jetpack_Options::get_option( $args[1] );
768
				if ( $value && is_array( $value ) ) {
769
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
770
				}
771
772
				Jetpack_Options::update_option( $args[1], $args[2] );
773
				/* translators: %1$s is the previous value, %2$s is the new value */
774
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
775
				break;
776
			case 'list':
777
				$options_compact     = Jetpack_Options::get_option_names();
778
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
779
				$options_private     = Jetpack_Options::get_option_names( 'private' );
780
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
781
782
				// Table headers
783
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
784
785
				// List out the options and their values
786
				// Tell them if the value is empty or not
787
				// Tell them if it's an array
788
				foreach ( $options as $option ) {
789
					$value = Jetpack_Options::get_option( $option );
790
					if ( ! $value ) {
791
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
792
						continue;
793
					}
794
795
					if ( ! is_array( $value ) ) {
796
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
797
					} elseif ( is_array( $value ) ) {
798
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
799
					}
800
				}
801
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
802
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
803
804
				WP_CLI::success(
805
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
806
					str_pad( 'wp jetpack options get', 26 ) . $option_text . "\n" .
807
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
808
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
809
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
810
				);
811
				break;
812
		}
813
	}
814
815
	/**
816
	 * Get the status of or start a new Jetpack sync.
817
	 *
818
	 * ## OPTIONS
819
	 *
820
	 * status   : Print the current sync status
821
	 * settings : Prints the current sync settings
822
	 * start    : Start a full sync from this site to WordPress.com
823
	 * enable   : Enables sync on the site
824
	 * disable  : Disable sync on a site
825
	 * reset    : Disables sync and Resets the sync queues on a site
826
	 *
827
	 * ## EXAMPLES
828
	 *
829
	 * wp jetpack sync status
830
	 * wp jetpack sync settings
831
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
832
	 * wp jetpack sync enable
833
	 * wp jetpack sync disable
834
	 * wp jetpack sync reset
835
	 * wp jetpack sync reset --queue=full or regular
836
	 *
837
	 * @synopsis <status|start> [--<field>=<value>]
838
	 */
839
	public function sync( $args, $assoc_args ) {
840
841
		$action = isset( $args[0] ) ? $args[0] : 'status';
842
843
		switch ( $action ) {
844
			case 'status':
845
				$status     = Actions::get_sync_status();
846
				$collection = array();
847
				foreach ( $status as $key => $item ) {
848
					$collection[] = array(
849
						'option' => $key,
850
						'value'  => is_scalar( $item ) ? $item : json_encode( $item ),
851
					);
852
				}
853
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
854
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
855
				break;
856
			case 'settings':
857
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
858
				foreach ( Settings::get_settings() as $setting => $item ) {
859
					$settings[] = array(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$settings was never initialized. Although not strictly required by PHP, it is generally a good practice to add $settings = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
860
						'setting' => $setting,
861
						'value'   => is_scalar( $item ) ? $item : json_encode( $item ),
862
					);
863
				}
864
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
0 ignored issues
show
Bug introduced by
The variable $settings 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...
865
866
			case 'disable':
867
				// Don't set it via the Settings since that also resets the queues.
868
				update_option( 'jetpack_sync_settings_disable', 1 );
869
				/* translators: %s is the site URL */
870
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
871
				break;
872
			case 'enable':
873
				Settings::update_settings( array( 'disable' => 0 ) );
874
				/* translators: %s is the site URL */
875
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
876
				break;
877
			case 'reset':
878
				// Don't set it via the Settings since that also resets the queues.
879
				update_option( 'jetpack_sync_settings_disable', 1 );
880
881
				/* translators: %s is the site URL */
882
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
883
				$listener = Listener::get_instance();
884
				if ( empty( $assoc_args['queue'] ) ) {
885
					$listener->get_sync_queue()->reset();
886
					$listener->get_full_sync_queue()->reset();
887
					/* translators: %s is the site URL */
888
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
889
					break;
890
				}
891
892
				if ( ! empty( $assoc_args['queue'] ) ) {
893
					switch ( $assoc_args['queue'] ) {
894 View Code Duplication
						case 'regular':
895
							$listener->get_sync_queue()->reset();
896
							/* translators: %s is the site URL */
897
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
898
							break;
899 View Code Duplication
						case 'full':
900
							$listener->get_full_sync_queue()->reset();
901
							/* translators: %s is the site URL */
902
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
903
							break;
904
						default:
905
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
906
							break;
907
					}
908
				}
909
910
				break;
911
			case 'start':
912
				if ( ! Actions::sync_allowed() ) {
913
					if ( Settings::get_setting( 'disable' ) ) {
914
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. It is currently disabled. Run `wp jetpack sync enable` to enable it.', 'jetpack' ) );
915
						return;
916
					}
917
					$connection = new Connection_Manager();
918
					if ( ! $connection->is_connected() ) {
919
						if ( ! doing_action( 'jetpack_site_registered' ) ) {
920
							WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
921
							return;
922
						}
923
					}
924
925
					$status = new Status();
926
927
					if ( $status->is_offline_mode() ) {
928
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in offline mode.', 'jetpack' ) );
929
						return;
930
					}
931
					if ( $status->is_staging_site() ) {
932
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in staging mode.', 'jetpack' ) );
933
						return;
934
					}
935
				}
936
				// Get the original settings so that we can restore them later
937
				$original_settings = Settings::get_settings();
938
939
				// Initialize sync settigns so we can sync as quickly as possible
940
				$sync_settings = wp_parse_args(
941
					array_intersect_key( $assoc_args, Settings::$valid_settings ),
942
					array(
0 ignored issues
show
Documentation introduced by
array('sync_wait_time' =...on' => HOUR_IN_SECONDS) is of type array<string,integer,{"s...d_duration":"integer"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
943
						'sync_wait_time'           => 0,
944
						'enqueue_wait_time'        => 0,
945
						'queue_max_writes_sec'     => 10000,
946
						'max_queue_size_full_sync' => 100000,
947
						'full_sync_send_duration'  => HOUR_IN_SECONDS,
948
					)
949
				);
950
				Settings::update_settings( $sync_settings );
0 ignored issues
show
Bug introduced by
It seems like $sync_settings defined by wp_parse_args(array_inte...n' => HOUR_IN_SECONDS)) on line 940 can also be of type null; however, Automattic\Jetpack\Sync\...ings::update_settings() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
951
952
				// Convert comma-delimited string of modules to an array
953 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
954
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
955
956
					// Convert the array so that the keys are the module name and the value is true to indicate
957
					// that we want to sync the module
958
					$modules = array_map( '__return_true', array_flip( $modules ) );
959
				}
960
961 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
962
					if (
963
						'users' === $module_name &&
964
						isset( $assoc_args[ $module_name ] ) &&
965
						'initial' === $assoc_args[ $module_name ]
966
					) {
967
						$modules['users'] = 'initial';
0 ignored issues
show
Bug introduced by
The variable $modules 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...
968
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
969
						$ids = explode( ',', $assoc_args[ $module_name ] );
970
						if ( count( $ids ) > 0 ) {
971
							$modules[ $module_name ] = $ids;
972
						}
973
					}
974
				}
975
976
				if ( empty( $modules ) ) {
977
					$modules = null;
978
				}
979
980
				// Kick off a full sync
981
				if ( Actions::do_full_sync( $modules ) ) {
982 View Code Duplication
					if ( $modules ) {
983
						/* translators: %s is a comma separated list of Jetpack modules */
984
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
985
					} else {
986
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
987
					}
988 View Code Duplication
				} else {
989
990
					// Reset sync settings to original.
991
					Settings::update_settings( $original_settings );
992
993
					if ( $modules ) {
994
						/* translators: %s is a comma separated list of Jetpack modules */
995
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
996
					} else {
997
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
998
					}
999
				}
1000
1001
				// Keep sending to WPCOM until there's nothing to send
1002
				$i = 1;
1003
				do {
1004
					$result = Actions::$sender->do_full_sync();
1005
					if ( is_wp_error( $result ) ) {
1006
						$queue_empty_error = ( 'empty_queue_full_sync' == $result->get_error_code() );
1007
						if ( ! $queue_empty_error || ( $queue_empty_error && ( 1 == $i ) ) ) {
1008
							/* translators: %s is an error code  */
1009
							WP_CLI::error( sprintf( __( 'Sync errored with code: %s', 'jetpack' ), $result->get_error_code() ) );
1010
						}
1011
					} else {
1012
						if ( 1 == $i ) {
1013
							WP_CLI::log( __( 'Sent data to WordPress.com', 'jetpack' ) );
1014
						} else {
1015
							WP_CLI::log( __( 'Sent more data to WordPress.com', 'jetpack' ) );
1016
						}
1017
1018
						// Immediate Full Sync does not wait for WP.com to process data so we need to enforce a wait.
1019
						if ( false !== strpos( get_class( Modules::get_module( 'full-sync' ) ), 'Full_Sync_Immediately' ) ) {
1020
							sleep( 15 );
1021
						}
1022
					}
1023
					$i++;
1024
				} while ( $result && ! is_wp_error( $result ) );
1025
1026
				// Reset sync settings to original.
1027
				Settings::update_settings( $original_settings );
1028
1029
				WP_CLI::success( __( 'Finished syncing to WordPress.com', 'jetpack' ) );
1030
				break;
1031
		}
1032
	}
1033
1034
	/**
1035
	 * List the contents of a specific Jetpack sync queue.
1036
	 *
1037
	 * ## OPTIONS
1038
	 *
1039
	 * peek : List the 100 front-most items on the queue.
1040
	 *
1041
	 * ## EXAMPLES
1042
	 *
1043
	 * wp jetpack sync_queue full_sync peek
1044
	 *
1045
	 * @synopsis <incremental|full_sync> <peek>
1046
	 */
1047
	public function sync_queue( $args, $assoc_args ) {
1048
		if ( ! Actions::sync_allowed() ) {
1049
			WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site.', 'jetpack' ) );
1050
		}
1051
1052
		$queue_name = isset( $args[0] ) ? $args[0] : 'sync';
1053
		$action     = isset( $args[1] ) ? $args[1] : 'peek';
1054
1055
		// We map the queue name that way we can support more friendly queue names in the commands, but still use
1056
		// the queue name that the code expects.
1057
		$queue_name_map    = $allowed_queues = array(
0 ignored issues
show
Unused Code introduced by
$allowed_queues is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1058
			'incremental' => 'sync',
1059
			'full'        => 'full_sync',
1060
		);
1061
		$mapped_queue_name = isset( $queue_name_map[ $queue_name ] ) ? $queue_name_map[ $queue_name ] : $queue_name;
1062
1063
		switch ( $action ) {
1064
			case 'peek':
1065
				$queue = new Queue( $mapped_queue_name );
1066
				$items = $queue->peek( 100 );
1067
1068
				if ( empty( $items ) ) {
1069
					/* translators: %s is the name of the queue, either 'incremental' or 'full' */
1070
					WP_CLI::log( sprintf( __( 'Nothing is in the queue: %s', 'jetpack' ), $queue_name ) );
1071
				} else {
1072
					$collection = array();
1073
					foreach ( $items as $item ) {
1074
						$collection[] = array(
1075
							'action'          => $item[0],
1076
							'args'            => json_encode( $item[1] ),
1077
							'current_user_id' => $item[2],
1078
							'microtime'       => $item[3],
1079
							'importing'       => (string) $item[4],
1080
						);
1081
					}
1082
					WP_CLI\Utils\format_items(
1083
						'table',
1084
						$collection,
1085
						array(
1086
							'action',
1087
							'args',
1088
							'current_user_id',
1089
							'microtime',
1090
							'importing',
1091
						)
1092
					);
1093
				}
1094
				break;
1095
		}
1096
	}
1097
1098
	/**
1099
	 * Cancel's the current Jetpack plan granted by this partner, if applicable
1100
	 *
1101
	 * Returns success or error JSON
1102
	 *
1103
	 * <token_json>
1104
	 * : JSON blob of WPCOM API token
1105
	 *  [--partner_tracking_id=<partner_tracking_id>]
1106
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1107
	 *
1108
	 *  * @synopsis <token_json> [--partner_tracking_id=<partner_tracking_id>]
1109
	 */
1110
	public function partner_cancel( $args, $named_args ) {
1111
		list( $token_json ) = $args;
1112
1113 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1114
			/* translators: %s is the invalid JSON string */
1115
			$this->partner_provision_error( new WP_Error( 'missing_access_token', sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'missing_access_token'.

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

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

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

Loading history...
1116
		}
1117
1118
		if ( isset( $token->error ) ) {
1119
			$this->partner_provision_error( new WP_Error( $token->error, $token->message ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $token->error.

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

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

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

Loading history...
Bug introduced by
The variable $token 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...
1120
		}
1121
1122
		if ( ! isset( $token->access_token ) ) {
1123
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'missing_access_token'.

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

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

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

Loading history...
1124
		}
1125
1126
		if ( Jetpack::validate_sync_error_idc_option() ) {
1127
			$this->partner_provision_error(
1128
				new WP_Error(
1129
					'site_in_safe_mode',
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'site_in_safe_mode'.

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

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

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

Loading history...
1130
					esc_html__( 'Can not cancel a plan while in safe mode. See: https://jetpack.com/support/safe-mode/', 'jetpack' )
1131
				)
1132
			);
1133
		}
1134
1135
		$site_identifier = Jetpack_Options::get_option( 'id' );
1136
1137
		if ( ! $site_identifier ) {
1138
			$status          = new Status();
1139
			$site_identifier = $status->get_site_suffix();
1140
		}
1141
1142
		$request = array(
1143
			'headers' => array(
1144
				'Authorization' => 'Bearer ' . $token->access_token,
1145
				'Host'          => 'public-api.wordpress.com',
1146
			),
1147
			'timeout' => 60,
1148
			'method'  => 'POST',
1149
		);
1150
1151
		$url = sprintf( '%s/rest/v1.3/jpphp/%s/partner-cancel', $this->get_api_host(), $site_identifier );
1152 View Code Duplication
		if ( ! empty( $named_args ) && ! empty( $named_args['partner_tracking_id'] ) ) {
1153
			$url = esc_url_raw( add_query_arg( 'partner_tracking_id', $named_args['partner_tracking_id'], $url ) );
1154
		}
1155
1156
		$result = Client::_wp_remote_request( $url, $request );
1157
1158
		Jetpack_Options::delete_option( 'onboarding' );
1159
1160
		if ( is_wp_error( $result ) ) {
1161
			$this->partner_provision_error( $result );
1162
		}
1163
1164
		WP_CLI::log( wp_remote_retrieve_body( $result ) );
1165
	}
1166
1167
	/**
1168
	 * Provision a site using a Jetpack Partner license
1169
	 *
1170
	 * Returns JSON blob
1171
	 *
1172
	 * ## OPTIONS
1173
	 *
1174
	 * <token_json>
1175
	 * : JSON blob of WPCOM API token
1176
	 * [--plan=<plan_name>]
1177
	 * : Slug of the requested plan, e.g. premium
1178
	 * [--wpcom_user_id=<user_id>]
1179
	 * : WordPress.com ID of user to connect as (must be whitelisted against partner key)
1180
	 * [--wpcom_user_email=<wpcom_user_email>]
1181
	 * : Override the email we send to WordPress.com for registration
1182
	 * [--onboarding=<onboarding>]
1183
	 * : Guide the user through an onboarding wizard
1184
	 * [--force_register=<register>]
1185
	 * : Whether to force a site to register
1186
	 * [--force_connect=<force_connect>]
1187
	 * : Force JPS to not reuse existing credentials
1188
	 * [--home_url=<home_url>]
1189
	 * : Overrides the home option via the home_url filter, or the WP_HOME constant
1190
	 * [--site_url=<site_url>]
1191
	 * : Overrides the siteurl option via the site_url filter, or the WP_SITEURL constant
1192
	 * [--partner_tracking_id=<partner_tracking_id>]
1193
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1194
	 *
1195
	 * ## EXAMPLES
1196
	 *
1197
	 *     $ wp jetpack partner_provision '{ some: "json" }' premium 1
1198
	 *     { success: true }
1199
	 *
1200
	 * @synopsis <token_json> [--wpcom_user_id=<user_id>] [--plan=<plan_name>] [--onboarding=<onboarding>] [--force_register=<register>] [--force_connect=<force_connect>] [--home_url=<home_url>] [--site_url=<site_url>] [--wpcom_user_email=<wpcom_user_email>] [--partner_tracking_id=<partner_tracking_id>]
1201
	 */
1202
	public function partner_provision( $args, $named_args ) {
1203
		list( $token_json ) = $args;
1204
1205 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1206
			/* translators: %s is the invalid JSON string */
1207
			$this->partner_provision_error( new WP_Error( 'missing_access_token', sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'missing_access_token'.

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

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

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

Loading history...
1208
		}
1209
1210
		if ( isset( $token->error ) ) {
1211
			$message = isset( $token->message )
0 ignored issues
show
Bug introduced by
The variable $token 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...
1212
				? $token->message
1213
				: '';
1214
			$this->partner_provision_error( new WP_Error( $token->error, $message ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with $token->error.

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

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

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

Loading history...
1215
		}
1216
1217
		if ( ! isset( $token->access_token ) ) {
1218
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'missing_access_token'.

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

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

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

Loading history...
1219
		}
1220
1221
		require_once JETPACK__PLUGIN_DIR . '_inc/class.jetpack-provision.php';
1222
1223
		$body_json = Jetpack_Provision::partner_provision( $token->access_token, $named_args );
1224
1225
		if ( is_wp_error( $body_json ) ) {
1226
			error_log(
1227
				json_encode(
1228
					array(
1229
						'success'       => false,
1230
						'error_code'    => $body_json->get_error_code(),
0 ignored issues
show
Bug introduced by
The method get_error_code() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1231
						'error_message' => $body_json->get_error_message(),
0 ignored issues
show
Bug introduced by
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1232
					)
1233
				)
1234
			);
1235
			exit( 1 );
1236
		}
1237
1238
		WP_CLI::log( json_encode( $body_json ) );
1239
	}
1240
1241
	/**
1242
	 * Manages your Jetpack sitemap
1243
	 *
1244
	 * ## OPTIONS
1245
	 *
1246
	 * rebuild : Rebuild all sitemaps
1247
	 * --purge : if set, will remove all existing sitemap data before rebuilding
1248
	 *
1249
	 * ## EXAMPLES
1250
	 *
1251
	 * wp jetpack sitemap rebuild
1252
	 *
1253
	 * @subcommand sitemap
1254
	 * @synopsis <rebuild> [--purge]
1255
	 */
1256
	public function sitemap( $args, $assoc_args ) {
1257
		if ( ! Jetpack::is_connection_ready() ) {
1258
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1259
		}
1260
		if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1261
			WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1262
		}
1263
		if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1264
			WP_CLI::error( __( 'Jetpack Sitemaps module is active, but unavailable. This can happen if your site is set to discourage search engine indexing. Please enable search engine indexing to allow sitemap generation.', 'jetpack' ) );
1265
		}
1266
1267
		if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1268
			$librarian = new Jetpack_Sitemap_Librarian();
1269
			$librarian->delete_all_stored_sitemap_data();
1270
		}
1271
1272
		$sitemap_builder = new Jetpack_Sitemap_Builder();
1273
		$sitemap_builder->update_sitemap();
1274
	}
1275
1276
	/**
1277
	 * Allows authorizing a user via the command line and will activate
1278
	 *
1279
	 * ## EXAMPLES
1280
	 *
1281
	 * wp jetpack authorize_user --token=123456789abcdef
1282
	 *
1283
	 * @synopsis --token=<value>
1284
	 */
1285
	public function authorize_user( $args, $named_args ) {
1286
		if ( ! is_user_logged_in() ) {
1287
			WP_CLI::error( __( 'Please select a user to authorize via the --user global argument.', 'jetpack' ) );
1288
		}
1289
1290
		if ( empty( $named_args['token'] ) ) {
1291
			WP_CLI::error( __( 'A non-empty token argument must be passed.', 'jetpack' ) );
1292
		}
1293
1294
		$is_connection_owner = ! Jetpack::connection()->has_connected_owner();
1295
		$current_user_id     = get_current_user_id();
1296
1297
		( new Tokens() )->update_user_token( $current_user_id, sprintf( '%s.%d', $named_args['token'], $current_user_id ), $is_connection_owner );
1298
1299
		WP_CLI::log( wp_json_encode( $named_args ) );
1300
1301
		if ( $is_connection_owner ) {
1302
			/**
1303
			 * Auto-enable SSO module for new Jetpack Start connections
1304
			*
1305
			* @since 5.0.0
1306
			*
1307
			* @param bool $enable_sso Whether to enable the SSO module. Default to true.
1308
			*/
1309
			$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
1310
			Jetpack::handle_post_authorization_actions( $enable_sso, false );
1311
1312
			/* translators: %d is a user ID */
1313
			WP_CLI::success( sprintf( __( 'Authorized %d and activated default modules.', 'jetpack' ), $current_user_id ) );
1314
		} else {
1315
			/* translators: %d is a user ID */
1316
			WP_CLI::success( sprintf( __( 'Authorized %d.', 'jetpack' ), $current_user_id ) );
1317
		}
1318
	}
1319
1320
	/**
1321
	 * Allows calling a WordPress.com API endpoint using the current blog's token.
1322
	 *
1323
	 * ## OPTIONS
1324
	 * --resource=<resource>
1325
	 * : The resource to call with the current blog's token, where `%d` represents the current blog's ID.
1326
	 *
1327
	 * [--api_version=<api_version>]
1328
	 * : The API version to query against.
1329
	 *
1330
	 * [--base_api_path=<base_api_path>]
1331
	 * : The base API path to query.
1332
	 * ---
1333
	 * default: rest
1334
	 * ---
1335
	 *
1336
	 * [--body=<body>]
1337
	 * : A JSON encoded string representing arguments to send in the body.
1338
	 *
1339
	 * [--field=<value>]
1340
	 * : Any number of arguments that should be passed to the resource.
1341
	 *
1342
	 * [--pretty]
1343
	 * : Will pretty print the results of a successful API call.
1344
	 *
1345
	 * [--strip-success]
1346
	 * : Will remove the green success label from successful API calls.
1347
	 *
1348
	 * ## EXAMPLES
1349
	 *
1350
	 * wp jetpack call_api --resource='/sites/%d'
1351
	 */
1352
	public function call_api( $args, $named_args ) {
1353
		if ( ! Jetpack::is_connection_ready() ) {
1354
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1355
		}
1356
1357
		$consumed_args = array(
1358
			'resource',
1359
			'api_version',
1360
			'base_api_path',
1361
			'body',
1362
			'pretty',
1363
		);
1364
1365
		// Get args that should be passed to resource.
1366
		$other_args = array_diff_key( $named_args, array_flip( $consumed_args ) );
1367
1368
		$decoded_body = ! empty( $named_args['body'] )
1369
			? json_decode( $named_args['body'], true )
1370
			: false;
1371
1372
		$resource_url = ( false === strpos( $named_args['resource'], '%d' ) )
1373
			? $named_args['resource']
1374
			: sprintf( $named_args['resource'], Jetpack_Options::get_option( 'id' ) );
1375
1376
		$response = Client::wpcom_json_api_request_as_blog(
1377
			$resource_url,
1378
			empty( $named_args['api_version'] ) ? Client::WPCOM_JSON_API_VERSION : $named_args['api_version'],
1379
			$other_args,
1380
			empty( $decoded_body ) ? null : $decoded_body,
1381
			empty( $named_args['base_api_path'] ) ? 'rest' : $named_args['base_api_path']
1382
		);
1383
1384
		if ( is_wp_error( $response ) ) {
1385
			WP_CLI::error(
1386
				sprintf(
1387
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an error code, %3$s is an error message. */
1388
					__( 'Request to %1$s returned an error: (%2$d) %3$s.', 'jetpack' ),
1389
					$resource_url,
1390
					$response->get_error_code(),
1391
					$response->get_error_message()
1392
				)
1393
			);
1394
		}
1395
1396
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1397
			WP_CLI::error(
1398
				sprintf(
1399
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an HTTP status code. */
1400
					__( 'Request to %1$s returned a non-200 response code: %2$d.', 'jetpack' ),
1401
					$resource_url,
1402
					wp_remote_retrieve_response_code( $response )
1403
				)
1404
			);
1405
		}
1406
1407
		$output = wp_remote_retrieve_body( $response );
1408
		if ( isset( $named_args['pretty'] ) ) {
1409
			$decoded_output = json_decode( $output );
1410
			if ( $decoded_output ) {
1411
				$output = wp_json_encode( $decoded_output, JSON_PRETTY_PRINT );
1412
			}
1413
		}
1414
1415
		if ( isset( $named_args['strip-success'] ) ) {
1416
			WP_CLI::log( $output );
1417
			WP_CLI::halt( 0 );
1418
		}
1419
1420
		WP_CLI::success( $output );
1421
	}
1422
1423
	/**
1424
	 * Allows uploading SSH Credentials to the current site for backups, restores, and security scanning.
1425
	 *
1426
	 * ## OPTIONS
1427
	 *
1428
	 * [--host=<host>]
1429
	 * : The SSH server's address.
1430
	 *
1431
	 * [--ssh-user=<user>]
1432
	 * : The username to use to log in to the SSH server.
1433
	 *
1434
	 * [--pass=<pass>]
1435
	 * : The password used to log in, if using a password. (optional)
1436
	 *
1437
	 * [--kpri=<kpri>]
1438
	 * : The private key used to log in, if using a private key. (optional)
1439
	 *
1440
	 * [--pretty]
1441
	 * : Will pretty print the results of a successful API call. (optional)
1442
	 *
1443
	 * [--strip-success]
1444
	 * : Will remove the green success label from successful API calls. (optional)
1445
	 *
1446
	 * ## EXAMPLES
1447
	 *
1448
	 * wp jetpack upload_ssh_creds --host=example.com --ssh-user=example --pass=password
1449
	 * wp jetpack updload_ssh_creds --host=example.com --ssh-user=example --kpri=key
1450
	 */
1451
	public function upload_ssh_creds( $args, $named_args ) {
1452
		if ( ! Jetpack::is_connection_ready() ) {
1453
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1454
		}
1455
1456
		$required_args = array(
1457
			'host',
1458
			'ssh-user',
1459
		);
1460
1461
		foreach ( $required_args as $arg ) {
1462
			if ( empty( $named_args[ $arg ] ) ) {
1463
				WP_CLI::error(
1464
					sprintf(
1465
						/* translators: %s is a slug, such as 'host'. */
1466
						__( '`%s` cannot be empty.', 'jetpack' ),
1467
						$arg
1468
					)
1469
				);
1470
			}
1471
		}
1472
1473
		if ( empty( $named_args['pass'] ) && empty( $named_args['kpri'] ) ) {
1474
			WP_CLI::error( __( 'Both `pass` and `kpri` fields cannot be blank.', 'jetpack' ) );
1475
		}
1476
1477
		$values = array(
1478
			'credentials' => array(
1479
				'site_url' => get_site_url(),
1480
				'abspath'  => ABSPATH,
1481
				'protocol' => 'ssh',
1482
				'port'     => 22,
1483
				'role'     => 'main',
1484
				'host'     => $named_args['host'],
1485
				'user'     => $named_args['ssh-user'],
1486
				'pass'     => empty( $named_args['pass'] ) ? '' : $named_args['pass'],
1487
				'kpri'     => empty( $named_args['kpri'] ) ? '' : $named_args['kpri'],
1488
			),
1489
		);
1490
1491
		$named_args = wp_parse_args(
1492
			array(
1493
				'resource'    => '/activity-log/%d/update-credentials',
1494
				'method'      => 'POST',
1495
				'api_version' => '1.1',
1496
				'body'        => wp_json_encode( $values ),
1497
				'timeout'     => 30,
1498
			),
1499
			$named_args
1500
		);
1501
1502
		self::call_api( $args, $named_args );
1503
	}
1504
1505
	/**
1506
	 * API wrapper for getting stats from the WordPress.com API for the current site.
1507
	 *
1508
	 * ## OPTIONS
1509
	 *
1510
	 * [--quantity=<quantity>]
1511
	 * : The number of units to include.
1512
	 * ---
1513
	 * default: 30
1514
	 * ---
1515
	 *
1516
	 * [--period=<period>]
1517
	 * : The unit of time to query stats for.
1518
	 * ---
1519
	 * default: day
1520
	 * options:
1521
	 *  - day
1522
	 *  - week
1523
	 *  - month
1524
	 *  - year
1525
	 * ---
1526
	 *
1527
	 * [--date=<date>]
1528
	 * : The latest date to return stats for. Ex. - 2018-01-01.
1529
	 *
1530
	 * [--pretty]
1531
	 * : Will pretty print the results of a successful API call.
1532
	 *
1533
	 * [--strip-success]
1534
	 * : Will remove the green success label from successful API calls.
1535
	 *
1536
	 * ## EXAMPLES
1537
	 *
1538
	 * wp jetpack get_stats
1539
	 */
1540
	public function get_stats( $args, $named_args ) {
1541
		$selected_args = array_intersect_key(
1542
			$named_args,
1543
			array_flip(
1544
				array(
1545
					'quantity',
1546
					'date',
1547
				)
1548
			)
1549
		);
1550
1551
		// The API expects unit, but period seems to be more correct.
1552
		$selected_args['unit'] = $named_args['period'];
1553
1554
		$command = sprintf(
1555
			'jetpack call_api --resource=/sites/%d/stats/%s',
1556
			Jetpack_Options::get_option( 'id' ),
1557
			add_query_arg( $selected_args, 'visits' )
1558
		);
1559
1560
		if ( isset( $named_args['pretty'] ) ) {
1561
			$command .= ' --pretty';
1562
		}
1563
1564
		if ( isset( $named_args['strip-success'] ) ) {
1565
			$command .= ' --strip-success';
1566
		}
1567
1568
		WP_CLI::runcommand(
1569
			$command,
1570
			array(
1571
				'launch' => false, // Use the current process.
1572
			)
1573
		);
1574
	}
1575
1576
	/**
1577
	 * Allows management of publicize connections.
1578
	 *
1579
	 * ## OPTIONS
1580
	 *
1581
	 * <list|disconnect>
1582
	 * : The action to perform.
1583
	 * ---
1584
	 * options:
1585
	 *   - list
1586
	 *   - disconnect
1587
	 * ---
1588
	 *
1589
	 * [<identifier>]
1590
	 * : The connection ID or service to perform an action on.
1591
	 *
1592
	 * [--format=<format>]
1593
	 * : Allows overriding the output of the command when listing connections.
1594
	 * ---
1595
	 * default: table
1596
	 * options:
1597
	 *   - table
1598
	 *   - json
1599
	 *   - csv
1600
	 *   - yaml
1601
	 *   - ids
1602
	 *   - count
1603
	 * ---
1604
	 *
1605
	 * ## EXAMPLES
1606
	 *
1607
	 *     # List all publicize connections.
1608
	 *     $ wp jetpack publicize list
1609
	 *
1610
	 *     # List publicize connections for a given service.
1611
	 *     $ wp jetpack publicize list twitter
1612
	 *
1613
	 *     # List all publicize connections for a given user.
1614
	 *     $ wp --user=1 jetpack publicize list
1615
	 *
1616
	 *     # List all publicize connections for a given user and service.
1617
	 *     $ wp --user=1 jetpack publicize list twitter
1618
	 *
1619
	 *     # Display details for a given connection.
1620
	 *     $ wp jetpack publicize list 123456
1621
	 *
1622
	 *     # Diconnection a given connection.
1623
	 *     $ wp jetpack publicize disconnect 123456
1624
	 *
1625
	 *     # Disconnect all connections.
1626
	 *     $ wp jetpack publicize disconnect all
1627
	 *
1628
	 *     # Disconnect all connections for a given service.
1629
	 *     $ wp jetpack publicize disconnect twitter
1630
	 */
1631
	public function publicize( $args, $named_args ) {
1632
		if ( ! Jetpack::connection()->has_connected_owner() ) {
1633
			WP_CLI::error( __( 'Publicize requires a user-level connection to WordPress.com', 'jetpack' ) );
1634
		}
1635
1636
		if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1637
			WP_CLI::error( __( 'The publicize module is not active.', 'jetpack' ) );
1638
		}
1639
1640
		if ( ( new Status() )->is_offline_mode() ) {
1641
			if (
1642
				! defined( 'JETPACK_DEV_DEBUG' ) &&
1643
				! has_filter( 'jetpack_development_mode' ) &&
1644
				! has_filter( 'jetpack_offline_mode' ) &&
1645
				false === strpos( site_url(), '.' )
1646
			) {
1647
				WP_CLI::error( __( "Jetpack is current in offline mode because the site url does not contain a '.', which often occurs when dynamically setting the WP_SITEURL constant. While in offline mode, the publicize module will not load.", 'jetpack' ) );
1648
			}
1649
1650
			WP_CLI::error( __( 'Jetpack is currently in offline mode, so the publicize module will not load.', 'jetpack' ) );
1651
		}
1652
1653
		if ( ! class_exists( 'Publicize' ) ) {
1654
			WP_CLI::error( __( 'The publicize module is not loaded.', 'jetpack' ) );
1655
		}
1656
1657
		$action        = $args[0];
1658
		$publicize     = new Publicize();
1659
		$identifier    = ! empty( $args[1] ) ? $args[1] : false;
1660
		$services      = array_keys( $publicize->get_services() );
1661
		$id_is_service = in_array( $identifier, $services, true );
1662
1663
		switch ( $action ) {
1664
			case 'list':
1665
				$connections_to_return = array();
1666
1667
				// For the CLI command, let's return all connections when a user isn't specified. This
1668
				// differs from the logic in the Publicize class.
1669
				$option_connections = is_user_logged_in()
1670
					? (array) $publicize->get_all_connections_for_user()
1671
					: (array) $publicize->get_all_connections();
1672
1673
				foreach ( $option_connections as $service_name => $connections ) {
1674
					foreach ( (array) $connections as $id => $connection ) {
1675
						$connection['id']        = $id;
1676
						$connection['service']   = $service_name;
1677
						$connections_to_return[] = $connection;
1678
					}
1679
				}
1680
1681
				if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1682
					$temp_connections      = $connections_to_return;
1683
					$connections_to_return = array();
1684
1685
					foreach ( $temp_connections as $connection ) {
1686
						if ( $identifier === $connection['service'] ) {
1687
							$connections_to_return[] = $connection;
1688
						}
1689
					}
1690
				}
1691
1692
				if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1693
					$connections_to_return = wp_list_filter( $connections_to_return, array( 'id' => $identifier ) );
1694
				}
1695
1696
				$expected_keys = array(
1697
					'id',
1698
					'service',
1699
					'user_id',
1700
					'provider',
1701
					'issued',
1702
					'expires',
1703
					'external_id',
1704
					'external_name',
1705
					'external_display',
1706
					'type',
1707
					'connection_data',
1708
				);
1709
1710
				// Somehow, a test site ended up in a state where $connections_to_return looked like:
1711
				// array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1712
				// This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1713
				// to minimize future issues, this nested loop will remove any connections that don't contain
1714
				// any keys that we expect.
1715
				foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1716
					foreach ( $expected_keys as $expected_key ) {
1717
						if ( ! isset( $connection[ $expected_key ] ) ) {
1718
							unset( $connections_to_return[ $connection_key ] );
1719
							continue;
1720
						}
1721
					}
1722
				}
1723
1724
				if ( empty( $connections_to_return ) ) {
1725
					return false;
1726
				}
1727
1728
				WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1729
				break; // list.
1730
			case 'disconnect':
1731
				if ( ! $identifier ) {
1732
					WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1733
				}
1734
1735
				// If the connection ID is 'all' then delete all connections. If the connection ID
1736
				// matches a service, delete all connections for that service.
1737
				if ( 'all' === $identifier || $id_is_service ) {
1738
					if ( 'all' === $identifier ) {
1739
						WP_CLI::log( __( "You're about to delete all publicize connections.", 'jetpack' ) );
1740
					} else {
1741
						/* translators: %s is a lowercase string for a social network. */
1742
						WP_CLI::log( sprintf( __( "You're about to delete all publicize connections to %s.", 'jetpack' ), $identifier ) );
1743
					}
1744
1745
					jetpack_cli_are_you_sure();
1746
1747
					$connections = array();
1748
					$service     = $identifier;
1749
1750
					$option_connections = is_user_logged_in()
1751
						? (array) $publicize->get_all_connections_for_user()
1752
						: (array) $publicize->get_all_connections();
1753
1754
					if ( 'all' === $service ) {
1755
						foreach ( (array) $option_connections as $service_name => $service_connections ) {
1756
							foreach ( $service_connections as $id => $connection ) {
1757
								$connections[ $id ] = $connection;
1758
							}
1759
						}
1760
					} elseif ( ! empty( $option_connections[ $service ] ) ) {
1761
						$connections = $option_connections[ $service ];
1762
					}
1763
1764
					if ( ! empty( $connections ) ) {
1765
						$count    = count( $connections );
1766
						$progress = \WP_CLI\Utils\make_progress_bar(
1767
							/* translators: %s is a lowercase string for a social network. */
1768
							sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1769
							$count
1770
						);
1771
1772
						foreach ( $connections as $id => $connection ) {
1773
							if ( false === $publicize->disconnect( false, $id ) ) {
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1774
								WP_CLI::error(
1775
									sprintf(
1776
										/* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1777
										__( 'Publicize connection %d could not be disconnected', 'jetpack' ),
1778
										$id
1779
									)
1780
								);
1781
							}
1782
1783
							$progress->tick();
1784
						}
1785
1786
						$progress->finish();
1787
1788
						if ( 'all' === $service ) {
1789
							WP_CLI::success( __( 'All publicize connections were successfully disconnected.', 'jetpack' ) );
1790
						} else {
1791
							/* translators: %s is a lowercase string for a social network. */
1792
							WP_CLI::success( __( 'All publicize connections to %s were successfully disconnected.', 'jetpack' ), $service );
1793
						}
1794
					}
1795
				} else {
1796
					if ( false !== $publicize->disconnect( false, $identifier ) ) {
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1797
						/* translators: %d is a numeric ID. Example: 1234. */
1798
						WP_CLI::success( sprintf( __( 'Publicize connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1799
					} else {
1800
						/* translators: %d is a numeric ID. Example: 1234. */
1801
						WP_CLI::error( sprintf( __( 'Publicize connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1802
					}
1803
				}
1804
				break; // disconnect.
1805
		}
1806
	}
1807
1808
	private function get_api_host() {
1809
		$env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1810
		return $env_api_host ? 'https://' . $env_api_host : JETPACK__WPCOM_JSON_API_BASE;
1811
	}
1812
1813
	private function partner_provision_error( $error ) {
1814
		WP_CLI::log(
1815
			json_encode(
1816
				array(
1817
					'success'       => false,
1818
					'error_code'    => $error->get_error_code(),
1819
					'error_message' => $error->get_error_message(),
1820
				)
1821
			)
1822
		);
1823
		exit( 1 );
1824
	}
1825
1826
	/**
1827
	 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1828
	 *
1829
	 * ## TYPES
1830
	 *
1831
	 * block: it creates a Jetpack block. All files will be created in a directory under extensions/blocks named based on the block title or a specific given slug.
1832
	 *
1833
	 * ## BLOCK TYPE OPTIONS
1834
	 *
1835
	 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1836
	 * The title is also used to create the slug and the edit PHP class name. If it's something like "Logo gallery", the slug will be 'logo-gallery' and the class name will be LogoGalleryEdit.
1837
	 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1838
	 * --description: Allows to provide a text description of the block.
1839
	 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1840
	 * --variation: Allows to decide whether the block should be a production block, experimental, or beta. Defaults to Beta when arg not provided.
1841
	 *
1842
	 * ## BLOCK TYPE EXAMPLES
1843
	 *
1844
	 * wp jetpack scaffold block "Cool Block"
1845
	 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1846
	 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1847
	 * wp jetpack scaffold block "Jukebox" --variation="experimental"
1848
	 *
1849
	 * @subcommand scaffold block
1850
	 * @synopsis <type> <title> [--slug] [--description] [--keywords] [--variation]
1851
	 *
1852
	 * @param array $args       Positional parameters, when strings are passed, wrap them in quotes.
1853
	 * @param array $assoc_args Associative parameters like --slug="nice-block".
1854
	 */
1855
	public function scaffold( $args, $assoc_args ) {
1856
		// It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1857
		switch ( $args[0] ) {
1858
			case 'block':
1859
				$this->block( $args, $assoc_args );
1860
				break;
1861
			default:
1862
				/* translators: %s is the subcommand */
1863
				WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1864
				exit( 1 );
1865
		}
1866
	}
1867
1868
	/**
1869
	 * Creates the essential files in Jetpack to build a Gutenberg block.
1870
	 *
1871
	 * @param array $args       Positional parameters. Only one is used, that corresponds to the block title.
1872
	 * @param array $assoc_args Associative parameters defined in the scaffold() method.
1873
	 */
1874
	public function block( $args, $assoc_args ) {
1875 View Code Duplication
		if ( isset( $args[1] ) ) {
1876
			$title = ucwords( $args[1] );
1877
		} else {
1878
			WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
1879
			exit( 1 );
1880
		}
1881
1882
		$slug = isset( $assoc_args['slug'] )
1883
			? $assoc_args['slug']
1884
			: sanitize_title( $title );
1885
1886
		$variation_options = array( 'production', 'experimental', 'beta' );
1887
		$variation         = ( isset( $assoc_args['variation'] ) && in_array( $assoc_args['variation'], $variation_options, true ) )
1888
			? $assoc_args['variation']
1889
			: 'beta';
1890
1891
		if ( preg_match( '#^jetpack/#', $slug ) ) {
1892
			$slug = preg_replace( '#^jetpack/#', '', $slug );
1893
		}
1894
1895
		if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
1896
			WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
1897
		}
1898
1899
		global $wp_filesystem;
1900
		if ( ! WP_Filesystem() ) {
1901
			WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
1902
		}
1903
1904
		$path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
1905
1906
		if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
1907
			/* translators: %s is path to the conflicting block */
1908
			WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
1909
			exit( 1 );
1910
		}
1911
1912
		$wp_filesystem->mkdir( $path );
1913
1914
		$hasKeywords = isset( $assoc_args['keywords'] );
1915
1916
		$files = array(
1917
			"$path/$slug.php"     => $this->render_block_file(
1918
				'block-register-php',
1919
				array(
1920
					'slug'             => $slug,
1921
					'title'            => $title,
1922
					'underscoredSlug'  => str_replace( '-', '_', $slug ),
1923
					'underscoredTitle' => str_replace( ' ', '_', $title ),
1924
					'jetpackVersion'   => substr( JETPACK__VERSION, 0, strpos( JETPACK__VERSION, '.' ) ) . '.x',
1925
				)
1926
			),
1927
			"$path/index.js"      => $this->render_block_file(
1928
				'block-index-js',
1929
				array(
1930
					'slug'        => $slug,
1931
					'title'       => $title,
1932
					'description' => isset( $assoc_args['description'] )
1933
						? $assoc_args['description']
1934
						: $title,
1935
					'keywords'    => $hasKeywords
1936
					? array_map(
1937
						function( $keyword ) {
1938
								// Construction necessary for Mustache lists
1939
								return array( 'keyword' => trim( $keyword ) );
1940
						},
1941
						explode( ',', $assoc_args['keywords'], 3 )
1942
					)
1943
					: '',
1944
					'hasKeywords' => $hasKeywords,
1945
				)
1946
			),
1947
			"$path/editor.js"     => $this->render_block_file( 'block-editor-js' ),
1948
			"$path/editor.scss"   => $this->render_block_file(
1949
				'block-editor-scss',
1950
				array(
1951
					'slug'  => $slug,
1952
					'title' => $title,
1953
				)
1954
			),
1955
			"$path/edit.js"       => $this->render_block_file(
1956
				'block-edit-js',
1957
				array(
1958
					'title'     => $title,
1959
					'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
1960
				)
1961
			),
1962
			"$path/icon.js"       => $this->render_block_file( 'block-icon-js' ),
1963
			"$path/attributes.js" => $this->render_block_file( 'block-attributes-js' ),
1964
		);
1965
1966
		$files_written = array();
1967
1968
		foreach ( $files as $filename => $contents ) {
1969
			if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
1970
				$files_written[] = $filename;
1971
			} else {
1972
				/* translators: %s is a file name */
1973
				WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
1974
			}
1975
		}
1976
1977
		if ( empty( $files_written ) ) {
1978
			WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
1979
		} else {
1980
			// Load index.json and insert the slug of the new block in its block variation array.
1981
			$block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
1982
			$block_list      = $wp_filesystem->get_contents( $block_list_path );
1983
			if ( empty( $block_list ) ) {
1984
				/* translators: %s is the path to the file with the block list */
1985
				WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
1986
			} elseif ( false === stripos( $block_list, $slug ) ) {
1987
				$new_block_list                   = json_decode( $block_list );
1988
				$new_block_list->{ $variation }[] = $slug;
1989
1990
				// Format the JSON to match our coding standards.
1991
				$new_block_list_formatted = wp_json_encode( $new_block_list, JSON_PRETTY_PRINT ) . "\n";
1992
				$new_block_list_formatted = preg_replace_callback(
1993
					// Find all occurrences of multiples of 4 spaces a the start of the line.
1994
					'/^((?:    )+)/m',
1995
					function ( $matches ) {
1996
						// Replace each occurrence of 4 spaces with a tab character.
1997
						return str_repeat( "\t", substr_count( $matches[0], '    ' ) );
1998
					},
1999
					$new_block_list_formatted
2000
				);
2001
2002
				if ( ! $wp_filesystem->put_contents( $block_list_path, $new_block_list_formatted ) ) {
2003
					/* translators: %s is the path to the file with the block list */
2004
					WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
2005
				}
2006
			}
2007
2008
			if ( 'beta' === $variation || 'experimental' === $variation ) {
2009
				$block_constant = sprintf(
2010
					/* translators: the placeholder is a constant name */
2011
					esc_html__( 'To load the block, add the constant %1$s as true to your wp-config.php file', 'jetpack' ),
2012
					( 'beta' === $variation ? 'JETPACK_BETA_BLOCKS' : 'JETPACK_EXPERIMENTAL_BLOCKS' )
2013
				);
2014
			} else {
2015
				$block_constant = '';
2016
			}
2017
2018
			WP_CLI::success(
2019
				sprintf(
2020
					/* translators: the placeholders are a human readable title, and a series of words separated by dashes */
2021
					esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
2022
					"--------------------------------------------------------------------------------------------------------------------\n" .
2023
					/* translators: the placeholder is a directory path */
2024
					esc_html__( 'The files were created at %3$s', 'jetpack' ) . "\n" .
2025
					esc_html__( 'To start using the block, build the blocks with yarn run build-extensions', 'jetpack' ) . "\n" .
2026
					/* translators: the placeholder is a file path */
2027
					esc_html__( 'The block slug has been added to the %4$s list at %5$s', 'jetpack' ) . "\n" .
2028
					'%6$s' . "\n" .
2029
					/* translators: the placeholder is a URL */
2030
					"\n" . esc_html__( 'Read more at %7$s', 'jetpack' ) . "\n",
2031
					$title,
2032
					$slug,
2033
					$path,
2034
					$variation,
2035
					$block_list_path,
2036
					$block_constant,
2037
					'https://github.com/Automattic/jetpack/blob/master/extensions/README.md#develop-new-blocks'
2038
				) . '--------------------------------------------------------------------------------------------------------------------'
2039
			);
2040
		}
2041
	}
2042
2043
	/**
2044
	 * Built the file replacing the placeholders in the template with the data supplied.
2045
	 *
2046
	 * @param string $template
2047
	 * @param array  $data
2048
	 *
2049
	 * @return string mixed
2050
	 */
2051
	private static function render_block_file( $template, $data = array() ) {
2052
		return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
2053
	}
2054
}
2055
2056
/*
2057
 * Standard "ask for permission to continue" function.
2058
 * If action cancelled, ask if they need help.
2059
 *
2060
 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
2061
 *
2062
 * @param $flagged   bool   false = normal option | true = flagged by get_jetpack_options_for_reset()
2063
 * @param $error_msg string (optional)
2064
 */
2065
function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
2066
	$cli = new Jetpack_CLI();
2067
2068
	// Default cancellation message
2069
	if ( ! $error_msg ) {
2070
		$error_msg =
2071
			__( 'Action cancelled. Have a question?', 'jetpack' )
2072
			. ' '
2073
			. $cli->green_open
2074
			. 'jetpack.com/support'
2075
			. $cli->color_close;
2076
	}
2077
2078
	if ( ! $flagged ) {
2079
		$prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
2080
	} else {
2081
		$prompt_message = _x( 'Are you sure? Modifying this option may disrupt your Jetpack connection.  Type "yes" to continue.', '"yes" is a command - do not translate.', 'jetpack' );
2082
	}
2083
2084
	WP_CLI::line( $prompt_message );
2085
	$handle = fopen( 'php://stdin', 'r' );
2086
	$line   = fgets( $handle );
2087
	if ( 'yes' != trim( $line ) ) {
2088
		WP_CLI::error( $error_msg );
2089
	}
2090
}
2091