Jetpack_CLI::block()   F
last analyzed

Complexity

Conditions 21
Paths 1793

Size

Total Lines 168

Duplication

Lines 6
Ratio 3.57 %

Importance

Changes 0
Metric Value
cc 21
nc 1793
nop 2
dl 6
loc 168
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Identity_Crisis;
7
use Automattic\Jetpack\Status;
8
use Automattic\Jetpack\Sync\Actions;
9
use Automattic\Jetpack\Sync\Listener;
10
use Automattic\Jetpack\Sync\Modules;
11
use Automattic\Jetpack\Sync\Queue;
12
use Automattic\Jetpack\Sync\Settings;
13
14
WP_CLI::add_command( 'jetpack', 'Jetpack_CLI' );
15
16
/**
17
 * Control your local Jetpack installation.
18
 */
19
class Jetpack_CLI extends WP_CLI_Command {
20
	// Aesthetics.
21
	public $green_open  = "\033[32m";
22
	public $red_open    = "\033[31m";
23
	public $yellow_open = "\033[33m";
24
	public $color_close = "\033[0m";
25
26
	/**
27
	 * Get Jetpack Details
28
	 *
29
	 * ## OPTIONS
30
	 *
31
	 * empty: Leave it empty for basic stats
32
	 *
33
	 * full: View full stats.  It's the data from the heartbeat
34
	 *
35
	 * ## EXAMPLES
36
	 *
37
	 * wp jetpack status
38
	 * wp jetpack status full
39
	 */
40
	public function status( $args, $assoc_args ) {
41
		jetpack_require_lib( 'debugger' );
42
43
		/* translators: %s is the site URL */
44
		WP_CLI::line( sprintf( __( 'Checking status for %s', 'jetpack' ), esc_url( get_home_url() ) ) );
45
46 View Code Duplication
		if ( isset( $args[0] ) && 'full' !== $args[0] ) {
47
			/* translators: %s is a command like "prompt" */
48
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $args[0] ) );
49
		}
50
51
		$master_user_email = Jetpack::get_master_user_email();
52
53
		$cxntests = new Jetpack_Cxn_Tests();
54
55
		if ( $cxntests->pass() ) {
56
			$cxntests->output_results_for_cli();
57
58
			WP_CLI::success( __( 'Jetpack is currently connected to WordPress.com', 'jetpack' ) );
59
		} else {
60
			$error = array();
61
			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...
62
				$error[] = $fail['name'] . ': ' . $fail['message'];
63
			}
64
			WP_CLI::error_multi_line( $error );
65
66
			$cxntests->output_results_for_cli();
67
68
			WP_CLI::error( __( 'One or more tests did not pass. Please investigate!', 'jetpack' ) ); // Exit CLI.
69
		}
70
71
		/* translators: %s is current version of Jetpack, for example 7.3 */
72
		WP_CLI::line( sprintf( __( 'The Jetpack Version is %s', 'jetpack' ), JETPACK__VERSION ) );
73
		/* translators: %d is WP.com ID of this blog */
74
		WP_CLI::line( sprintf( __( 'The WordPress.com blog_id is %d', 'jetpack' ), Jetpack_Options::get_option( 'id' ) ) );
75
		/* translators: %s is the email address of the connection owner */
76
		WP_CLI::line( sprintf( __( 'The WordPress.com account for the primary connection is %s', 'jetpack' ), $master_user_email ) );
77
78
		/*
79
		 * Are they asking for all data?
80
		 *
81
		 * Loop through heartbeat data and organize by priority.
82
		 */
83
		$all_data = ( isset( $args[0] ) && 'full' == $args[0] ) ? 'full' : false;
84
		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...
85
			// Heartbeat data
86
			WP_CLI::line( "\n" . __( 'Additional data: ', 'jetpack' ) );
87
88
			// Get the filtered heartbeat data.
89
			// Filtered so we can color/list by severity
90
			$stats = Jetpack::jetpack_check_heartbeat_data();
91
92
			// Display red flags first
93
			foreach ( $stats['bad'] as $stat => $value ) {
94
				printf( "$this->red_open%-'.16s %s $this->color_close\n", $stat, $value );
95
			}
96
97
			// Display caution warnings next
98
			foreach ( $stats['caution'] as $stat => $value ) {
99
				printf( "$this->yellow_open%-'.16s %s $this->color_close\n", $stat, $value );
100
			}
101
102
			// The rest of the results are good!
103
			foreach ( $stats['good'] as $stat => $value ) {
104
105
				// Modules should get special spacing for aestetics
106
				if ( strpos( $stat, 'odule-' ) ) {
107
					printf( "%-'.30s %s\n", $stat, $value );
108
					usleep( 4000 ); // For dramatic effect lolz
109
					continue;
110
				}
111
				printf( "%-'.16s %s\n", $stat, $value );
112
				usleep( 4000 ); // For dramatic effect lolz
113
			}
114
		} else {
115
			// Just the basics
116
			WP_CLI::line( "\n" . _x( "View full status with 'wp jetpack status full'", '"wp jetpack status full" is a command - do not translate', 'jetpack' ) );
117
		}
118
	}
119
120
	/**
121
	 * Tests the active connection
122
	 *
123
	 * 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.
124
	 *
125
	 * ## EXAMPLES
126
	 *
127
	 * wp jetpack test-connection
128
	 *
129
	 * @subcommand test-connection
130
	 */
131
	public function test_connection( $args, $assoc_args ) {
132
133
		/* translators: %s is the site URL */
134
		WP_CLI::line( sprintf( __( 'Testing connection for %s', 'jetpack' ), esc_url( get_site_url() ) ) );
135
136
		if ( ! Jetpack::is_connection_ready() ) {
137
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
138
		}
139
140
		$response = Client::wpcom_json_api_request_as_blog(
141
			sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
142
			Client::WPCOM_JSON_API_VERSION
143
		);
144
145
		if ( is_wp_error( $response ) ) {
146
			/* translators: %1$s is the error code, %2$s is the error message */
147
			WP_CLI::error( sprintf( __( 'Failed to test connection (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() ) );
148
		}
149
150
		$body = wp_remote_retrieve_body( $response );
151
		if ( ! $body ) {
152
			WP_CLI::error( __( 'Failed to test connection (empty response body)', 'jetpack' ) );
153
		}
154
155
		$result       = json_decode( $body );
156
		$is_connected = (bool) $result->connected;
157
		$message      = $result->message;
158
159
		if ( $is_connected ) {
160
			WP_CLI::success( $message );
161
		} else {
162
			WP_CLI::error( $message );
163
		}
164
	}
165
166
	/**
167
	 * Disconnect Jetpack Blogs or Users
168
	 *
169
	 * ## OPTIONS
170
	 *
171
	 * blog: Disconnect the entire blog.
172
	 *
173
	 * user <user_identifier>: Disconnect a specific user from WordPress.com.
174
	 *
175
	 * [--force]
176
	 * If the user ID provided is the connection owner, it will only be disconnected if --force is passed
177
	 *
178
	 * ## EXAMPLES
179
	 *
180
	 * wp jetpack disconnect blog
181
	 * wp jetpack disconnect user 13
182
	 * wp jetpack disconnect user 1 --force
183
	 * wp jetpack disconnect user username
184
	 * wp jetpack disconnect user [email protected]
185
	 *
186
	 * @synopsis <blog|user> [<user_identifier>] [--force]
187
	 */
188
	public function disconnect( $args, $assoc_args ) {
189
		if ( ! Jetpack::is_connection_ready() ) {
190
			WP_CLI::success( __( 'The site is not currently connected, so nothing to do!', 'jetpack' ) );
191
			return;
192
		}
193
194
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
195 View Code Duplication
		if ( ! in_array( $action, array( 'blog', 'user', 'prompt' ) ) ) {
196
			/* translators: %s is a command like "prompt" */
197
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
198
		}
199
200
		if ( in_array( $action, array( 'user' ) ) ) {
201
			if ( isset( $args[1] ) ) {
202
				$user_id = $args[1];
203
				if ( ctype_digit( $user_id ) ) {
204
					$field   = 'id';
205
					$user_id = (int) $user_id;
206
				} elseif ( is_email( $user_id ) ) {
207
					$field   = 'email';
208
					$user_id = sanitize_user( $user_id, true );
209
				} else {
210
					$field   = 'login';
211
					$user_id = sanitize_user( $user_id, true );
212
				}
213
				if ( ! $user = get_user_by( $field, $user_id ) ) {
214
					WP_CLI::error( __( 'Please specify a valid user.', 'jetpack' ) );
215
				}
216
			} else {
217
				WP_CLI::error( __( 'Please specify a user by either ID, username, or email.', 'jetpack' ) );
218
			}
219
		}
220
221
		$force_user_disconnect = ! empty( $assoc_args['force'] );
222
223
		switch ( $action ) {
224
			case 'blog':
225
				Jetpack::log( 'disconnect' );
226
				Jetpack::disconnect();
227
				WP_CLI::success(
228
					sprintf(
229
						/* translators: %s is the site URL */
230
						__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
231
						esc_url( get_site_url() )
232
					)
233
				);
234
				break;
235
			case 'user':
236
				$connection_manager = new Connection_Manager( 'jetpack' );
237
				$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...
238
				if ( $disconnected ) {
239
					Jetpack::log( 'unlink', $user->ID );
240
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
241
				} else {
242
					if ( ! $connection_manager->is_user_connected( $user->ID ) ) {
243
						/* translators: %s is a username */
244
						$error_message = sprintf( __( 'User %s could not be disconnected because it is not connected!', 'jetpack' ), "{$user->data->user_login} <{$user->data->user_email}>" );
245
					} elseif ( ! $force_user_disconnect && $connection_manager->is_connection_owner( $user->ID ) ) {
246
						/* translators: %s is a username */
247
						$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}>" );
248
					} else {
249
						/* translators: %s is a username */
250
						$error_message = sprintf( __( 'User %s could not be disconnected.', 'jetpack' ), "{$user->data->user_login} <{$user->data->user_email}>" );
251
					}
252
					WP_CLI::error( $error_message );
253
				}
254
				break;
255
			case 'prompt':
256
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
257
				break;
258
		}
259
	}
260
261
	/**
262
	 * Reset Jetpack options and settings to default
263
	 *
264
	 * ## OPTIONS
265
	 *
266
	 * modules: Resets modules to default state ( get_default_modules() )
267
	 *
268
	 * options: Resets all Jetpack options except:
269
	 *  - All private options (Blog token, user token, etc...)
270
	 *  - id (The Client ID/WP.com Blog ID of this site)
271
	 *  - master_user
272
	 *  - version
273
	 *  - activated
274
	 *
275
	 * ## EXAMPLES
276
	 *
277
	 * wp jetpack reset options
278
	 * wp jetpack reset modules
279
	 * wp jetpack reset sync-checksum --dry-run --offset=0
280
	 *
281
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
282
	 */
283
	public function reset( $args, $assoc_args ) {
284
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
285 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
286
			/* translators: %s is a command like "prompt" */
287
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
288
		}
289
290
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
291
292 View Code Duplication
		if ( $is_dry_run ) {
293
			WP_CLI::warning(
294
				__( "\nThis is a dry run.\n", 'jetpack' ) .
295
				__( "No actions will be taken.\n", 'jetpack' ) .
296
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
297
			);
298
		} else {
299
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
300
			jetpack_cli_are_you_sure();
301
		}
302
303
		switch ( $action ) {
304
			case 'options':
305
				$options_to_reset = Jetpack_Options::get_options_for_reset();
306
				// Reset the Jetpack options
307
				WP_CLI::line(
308
					sprintf(
309
						/* translators: %s is the site URL */
310
						__( "Resetting Jetpack Options for %s...\n", 'jetpack' ),
311
						esc_url( get_site_url() )
312
					)
313
				);
314
				sleep( 1 ); // Take a breath
315 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
316
					if ( ! $is_dry_run ) {
317
						Jetpack_Options::delete_option( $option_to_reset );
318
						usleep( 100000 );
319
					}
320
321
					/* translators: This is the result of an action. The option named %s was reset */
322
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
323
				}
324
325
				// Reset the WP options
326
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", 'jetpack' ) );
327
				usleep( 500000 ); // Take a breath
328 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
329
					if ( ! $is_dry_run ) {
330
						delete_option( $option_to_reset );
331
						usleep( 100000 );
332
					}
333
					/* translators: This is the result of an action. The option named %s was reset */
334
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
335
				}
336
337
				// Reset to default modules
338
				WP_CLI::line( __( "Resetting default modules...\n", 'jetpack' ) );
339
				usleep( 500000 ); // Take a breath
340
				$default_modules = Jetpack::get_default_modules();
341
				if ( ! $is_dry_run ) {
342
					Jetpack::update_active_modules( $default_modules );
343
				}
344
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
345
				break;
346 View Code Duplication
			case 'modules':
347
				if ( ! $is_dry_run ) {
348
					$default_modules = Jetpack::get_default_modules();
349
					Jetpack::update_active_modules( $default_modules );
350
				}
351
352
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
353
				break;
354
			case 'prompt':
355
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
356
				break;
357
			case 'sync-checksum':
358
				$option = 'jetpack_callables_sync_checksum';
359
360
				if ( is_multisite() ) {
361
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
362
363
					/*
364
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
365
					 * Offset can be used to paginate and try to clean up more sites.
366
					 */
367
					$sites       = get_sites(
368
						array(
369
							'number' => 1000,
370
							'offset' => $offset,
371
						)
372
					);
373
					$count_fixes = 0;
374
					foreach ( $sites as $site ) {
375
						switch_to_blog( $site->blog_id );
376
						$count = self::count_option( $option );
377
						if ( $count > 1 ) {
378
							if ( ! $is_dry_run ) {
379
								delete_option( $option );
380
							}
381
							WP_CLI::line(
382
								sprintf(
383
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
384
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
385
									$count,
386
									$option,
387
									"{$site->domain}{$site->path}"
388
								)
389
							);
390
							$count_fixes++;
391
							if ( ! $is_dry_run ) {
392
								/*
393
								 * We could be deleting a lot of options rows at the same time.
394
								 * Allow some time for replication to catch up.
395
								 */
396
								sleep( 3 );
397
							}
398
						}
399
400
						restore_current_blog();
401
					}
402
					if ( $count_fixes ) {
403
						WP_CLI::success(
404
							sprintf(
405
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
406
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
407
								$option,
408
								$count_fixes
409
							)
410
						);
411
					} else {
412
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
413
					}
414
					return;
415
				}
416
417
				$count = self::count_option( $option );
418
				if ( $count > 1 ) {
419
					if ( ! $is_dry_run ) {
420
						delete_option( $option );
421
					}
422
					WP_CLI::success(
423
						sprintf(
424
							/* translators: %1$d is a number, %2$s is the name of an option. */
425
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
426
							$count,
427
							$option
428
						)
429
					);
430
					return;
431
				}
432
433
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
434
				break;
435
436
		}
437
	}
438
439
	/**
440
	 * Return the number of times an option appears
441
	 * Normally an option would only appear 1 since the option key is supposed to be unique
442
	 * but if a site hasn't updated the DB schema then that would not be the case.
443
	 *
444
	 * @param string $option Option name.
445
	 *
446
	 * @return int
447
	 */
448
	private static function count_option( $option ) {
449
		global $wpdb;
450
		return (int) $wpdb->get_var(
451
			$wpdb->prepare(
452
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
453
				$option
454
			)
455
		);
456
457
	}
458
459
	/**
460
	 * Manage Jetpack Modules
461
	 *
462
	 * ## OPTIONS
463
	 *
464
	 * <list|activate|deactivate|toggle>
465
	 * : The action to take.
466
	 * ---
467
	 * default: list
468
	 * options:
469
	 *  - list
470
	 *  - activate
471
	 *  - deactivate
472
	 *  - toggle
473
	 * ---
474
	 *
475
	 * [<module_slug>]
476
	 * : The slug of the module to perform an action on.
477
	 *
478
	 * [--format=<format>]
479
	 * : Allows overriding the output of the command when listing modules.
480
	 * ---
481
	 * default: table
482
	 * options:
483
	 *  - table
484
	 *  - json
485
	 *  - csv
486
	 *  - yaml
487
	 *  - ids
488
	 *  - count
489
	 * ---
490
	 *
491
	 * ## EXAMPLES
492
	 *
493
	 * wp jetpack module list
494
	 * wp jetpack module list --format=json
495
	 * wp jetpack module activate stats
496
	 * wp jetpack module deactivate stats
497
	 * wp jetpack module toggle stats
498
	 * wp jetpack module activate all
499
	 * wp jetpack module deactivate all
500
	 */
501
	public function module( $args, $assoc_args ) {
502
		$action = isset( $args[0] ) ? $args[0] : 'list';
503
504
		if ( isset( $args[1] ) ) {
505
			$module_slug = $args[1];
506
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
507
				/* translators: %s is a module slug like "stats" */
508
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
509
			}
510
			if ( 'toggle' === $action ) {
511
				$action = Jetpack::is_module_active( $module_slug )
512
					? 'deactivate'
513
					: 'activate';
514
			}
515
			if ( 'all' === $args[1] ) {
516
				$action = ( 'deactivate' === $action )
517
					? 'deactivate_all'
518
					: 'activate_all';
519
			}
520
		} elseif ( 'list' !== $action ) {
521
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
522
			$action = 'list';
523
		}
524
525
		switch ( $action ) {
526
			case 'list':
527
				$modules_list = array();
528
				$modules      = Jetpack::get_available_modules();
529
				sort( $modules );
530
				foreach ( (array) $modules as $module_slug ) {
531
					if ( 'vaultpress' === $module_slug ) {
532
						continue;
533
					}
534
					$modules_list[] = array(
535
						'slug'   => $module_slug,
536
						'status' => Jetpack::is_module_active( $module_slug )
537
							? __( 'Active', 'jetpack' )
538
							: __( 'Inactive', 'jetpack' ),
539
					);
540
				}
541
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
542
				break;
543
			case 'activate':
544
				$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...
545
				Jetpack::log( 'activate', $module_slug );
546
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
547
					/* translators: %s is the name of a Jetpack module */
548
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
549
				} else {
550
					/* translators: %s is the name of a Jetpack module */
551
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
552
				}
553
				break;
554 View Code Duplication
			case 'activate_all':
555
				$modules = Jetpack::get_available_modules();
556
				Jetpack::update_active_modules( $modules );
557
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
558
				break;
559
			case 'deactivate':
560
				$module = Jetpack::get_module( $module_slug );
561
				Jetpack::log( 'deactivate', $module_slug );
562
				Jetpack::deactivate_module( $module_slug );
563
				/* translators: %s is the name of a Jetpack module */
564
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
565
				break;
566
			case 'deactivate_all':
567
				Jetpack::delete_active_modules();
568
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
569
				break;
570
			case 'toggle':
571
				// Will never happen, should have been handled above and changed to activate or deactivate.
572
				break;
573
		}
574
	}
575
576
	/**
577
	 * Manage Protect Settings
578
	 *
579
	 * ## OPTIONS
580
	 *
581
	 * allow: Add an IP address to an always allow list.  You can also read or clear the allow list.
582
	 *
583
	 *
584
	 * ## EXAMPLES
585
	 *
586
	 * wp jetpack protect allow <ip address>
587
	 * wp jetpack protect allow list
588
	 * wp jetpack protect allow clear
589
	 *
590
	 * @synopsis <allow> [<ip|ip_low-ip_high|list|clear>]
591
	 */
592
	public function protect( $args, $assoc_args ) {
593
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
594 View Code Duplication
		if ( ! in_array( $action, array( 'whitelist', 'allow' ), true ) ) { // Still allow "whitelist" for legacy support.
595
			/* translators: %s is a command like "prompt" */
596
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
597
		}
598
		// Check if module is active
599
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
600
			/* translators: %s is a module name */
601
			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__ ) );
602
		}
603
		if ( in_array( $action, array( 'allow', 'whitelist' ), true ) ) {
604
			if ( isset( $args[1] ) ) {
605
				$action = 'allow';
606
			} else {
607
				$action = 'prompt';
608
			}
609
		}
610
		switch ( $action ) {
611
			case 'allow':
612
				$allow         = array();
613
				$new_ip        = $args[1];
614
				$current_allow = get_site_option( 'jetpack_protect_whitelist', array() ); // @todo Update the option name.
615
616
				// Build array of IPs that are already on the allowed list.
617
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
618
				// low & high range params for jetpack_protect_ip_address_is_in_range();
619
				foreach ( $current_allow as $allowed ) {
620
621
					// IP ranges
622
					if ( $allowed->range ) {
623
624
						// Is it already on the allowed list?
625
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $allowed->range_low, $allowed->range_high ) ) {
626
							/* translators: %s is an IP address */
627
							WP_CLI::error( sprintf( __( '%s is already on the always allow list.', 'jetpack' ), $new_ip ) );
628
							break;
629
						}
630
						$allow[] = $allowed->range_low . ' - ' . $allowed->range_high;
631
632
					} else { // Individual IPs
633
634
						// Check if the IP is already on the allow list (single IP only).
635
						if ( $new_ip === $allowed->ip_address ) {
636
							/* translators: %s is an IP address */
637
							WP_CLI::error( sprintf( __( '%s is already on the always allow list.', 'jetpack' ), $new_ip ) );
638
							break;
639
						}
640
						$allow[] = $allowed->ip_address;
641
642
					}
643
				}
644
645
				/*
646
				 * List the allowed IPs.
647
				 * Done here because it's easier to read the $allow array after it's been rebuilt.
648
				 */
649
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
650
					if ( ! empty( $allow ) ) {
651
						WP_CLI::success( __( 'Here are your always allowed IPs:', 'jetpack' ) );
652
						foreach ( $allow as $ip ) {
653
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) );
654
						}
655
					} else {
656
						WP_CLI::line( __( 'Always allow list is empty.', 'jetpack' ) );
657
					}
658
					break;
659
				}
660
661
				/*
662
				 * Clear the always allow list.
663
				 */
664
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
665
					if ( ! empty( $allow ) ) {
666
						$allow = array();
667
						jetpack_protect_save_whitelist( $allow ); // @todo Need to update function name in the Protect module.
668
						WP_CLI::success( __( 'Cleared all IPs from the always allow list.', 'jetpack' ) );
669
					} else {
670
						WP_CLI::line( __( 'Always allow list is empty.', 'jetpack' ) );
671
					}
672
					break;
673
				}
674
675
				// Append new IP to allow array.
676
				array_push( $allow, $new_ip );
677
678
				// Save allow list if there are no errors.
679
				$result = jetpack_protect_save_whitelist( $allow ); // @todo Need to update function name in the Protect module.
680
				if ( is_wp_error( $result ) ) {
681
					WP_CLI::error( $result );
682
				}
683
684
				/* translators: %s is an IP address */
685
				WP_CLI::success( sprintf( __( '%s has been added to the always allowed list.', 'jetpack' ), $new_ip ) );
686
				break;
687
			case 'prompt':
688
				WP_CLI::error(
689
					__( 'No command found.', 'jetpack' ) . "\n" .
690
					__( 'Please enter the IP address you want to always allow.', 'jetpack' ) . "\n" .
691
					_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" .
692
					_x( "You can also 'list' or 'clear' the always allowed list.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
693
				);
694
				break;
695
		}
696
	}
697
698
	/**
699
	 * Manage Jetpack Options
700
	 *
701
	 * ## OPTIONS
702
	 *
703
	 * list   : List all jetpack options and their values
704
	 * delete : Delete an option
705
	 *          - can only delete options that are white listed.
706
	 * update : update an option
707
	 *          - can only update option strings
708
	 * get    : get the value of an option
709
	 *
710
	 * ## EXAMPLES
711
	 *
712
	 * wp jetpack options list
713
	 * wp jetpack options get    <option_name>
714
	 * wp jetpack options delete <option_name>
715
	 * wp jetpack options update <option_name> [<option_value>]
716
	 *
717
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
718
	 */
719
	public function options( $args, $assoc_args ) {
720
		$action         = isset( $args[0] ) ? $args[0] : 'list';
721
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
722
723
		// Is the option flagged as unsafe?
724
		$flagged = ! in_array( $args[1], $safe_to_modify );
725
726 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
727
			/* translators: %s is a command like "prompt" */
728
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
729
		}
730
731
		if ( isset( $args[0] ) ) {
732
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
733
				$action = 'get';
734
			} elseif ( 'delete' == $args[0] && isset( $args[1] ) ) {
735
				$action = 'delete';
736
			} elseif ( 'update' == $args[0] && isset( $args[1] ) ) {
737
				$action = 'update';
738
			} else {
739
				$action = 'list';
740
			}
741
		}
742
743
		// Bail if the option isn't found
744
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
745 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
746
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
747
		}
748
749
		// Let's print_r the option if it's an array
750
		// Used in the 'get' and 'list' actions
751
		$option = is_array( $option ) ? print_r( $option ) : $option;
752
753
		switch ( $action ) {
754
			case 'get':
755
				WP_CLI::success( "\t" . $option );
756
				break;
757
			case 'delete':
758
				jetpack_cli_are_you_sure( $flagged );
759
760
				Jetpack_Options::delete_option( $args[1] );
761
				/* translators: %s is the option name */
762
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
763
				break;
764
			case 'update':
765
				jetpack_cli_are_you_sure( $flagged );
766
767
				// Updating arrays would get pretty tricky...
768
				$value = Jetpack_Options::get_option( $args[1] );
769
				if ( $value && is_array( $value ) ) {
770
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
771
				}
772
773
				Jetpack_Options::update_option( $args[1], $args[2] );
774
				/* translators: %1$s is the previous value, %2$s is the new value */
775
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
776
				break;
777
			case 'list':
778
				$options_compact     = Jetpack_Options::get_option_names();
779
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
780
				$options_private     = Jetpack_Options::get_option_names( 'private' );
781
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
782
783
				// Table headers
784
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
785
786
				// List out the options and their values
787
				// Tell them if the value is empty or not
788
				// Tell them if it's an array
789
				foreach ( $options as $option ) {
790
					$value = Jetpack_Options::get_option( $option );
791
					if ( ! $value ) {
792
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
793
						continue;
794
					}
795
796
					if ( ! is_array( $value ) ) {
797
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
798
					} elseif ( is_array( $value ) ) {
799
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
800
					}
801
				}
802
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
803
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
804
805
				WP_CLI::success(
806
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
807
					str_pad( 'wp jetpack options get', 26 ) . $option_text . "\n" .
808
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
809
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
810
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
811
				);
812
				break;
813
		}
814
	}
815
816
	/**
817
	 * Get the status of or start a new Jetpack sync.
818
	 *
819
	 * ## OPTIONS
820
	 *
821
	 * status   : Print the current sync status
822
	 * settings : Prints the current sync settings
823
	 * start    : Start a full sync from this site to WordPress.com
824
	 * enable   : Enables sync on the site
825
	 * disable  : Disable sync on a site
826
	 * reset    : Disables sync and Resets the sync queues on a site
827
	 *
828
	 * ## EXAMPLES
829
	 *
830
	 * wp jetpack sync status
831
	 * wp jetpack sync settings
832
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
833
	 * wp jetpack sync enable
834
	 * wp jetpack sync disable
835
	 * wp jetpack sync reset
836
	 * wp jetpack sync reset --queue=full or regular
837
	 *
838
	 * @synopsis <status|start> [--<field>=<value>]
839
	 */
840
	public function sync( $args, $assoc_args ) {
841
842
		$action = isset( $args[0] ) ? $args[0] : 'status';
843
844
		switch ( $action ) {
845
			case 'status':
846
				$status     = Actions::get_sync_status();
847
				$collection = array();
848
				foreach ( $status as $key => $item ) {
849
					$collection[] = array(
850
						'option' => $key,
851
						'value'  => is_scalar( $item ) ? $item : json_encode( $item ),
852
					);
853
				}
854
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
855
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
856
				break;
857
			case 'settings':
858
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
859
				foreach ( Settings::get_settings() as $setting => $item ) {
860
					$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...
861
						'setting' => $setting,
862
						'value'   => is_scalar( $item ) ? $item : json_encode( $item ),
863
					);
864
				}
865
				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...
866
867
			case 'disable':
868
				// Don't set it via the Settings since that also resets the queues.
869
				update_option( 'jetpack_sync_settings_disable', 1 );
870
				/* translators: %s is the site URL */
871
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
872
				break;
873
			case 'enable':
874
				Settings::update_settings( array( 'disable' => 0 ) );
875
				/* translators: %s is the site URL */
876
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
877
				break;
878
			case 'reset':
879
				// Don't set it via the Settings since that also resets the queues.
880
				update_option( 'jetpack_sync_settings_disable', 1 );
881
882
				/* translators: %s is the site URL */
883
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
884
				$listener = Listener::get_instance();
885
				if ( empty( $assoc_args['queue'] ) ) {
886
					$listener->get_sync_queue()->reset();
887
					$listener->get_full_sync_queue()->reset();
888
					/* translators: %s is the site URL */
889
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
890
					break;
891
				}
892
893
				if ( ! empty( $assoc_args['queue'] ) ) {
894
					switch ( $assoc_args['queue'] ) {
895 View Code Duplication
						case 'regular':
896
							$listener->get_sync_queue()->reset();
897
							/* translators: %s is the site URL */
898
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
899
							break;
900 View Code Duplication
						case 'full':
901
							$listener->get_full_sync_queue()->reset();
902
							/* translators: %s is the site URL */
903
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
904
							break;
905
						default:
906
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
907
							break;
908
					}
909
				}
910
911
				break;
912
			case 'start':
913
				if ( ! Actions::sync_allowed() ) {
914
					if ( Settings::get_setting( 'disable' ) ) {
915
						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' ) );
916
						return;
917
					}
918
					$connection = new Connection_Manager();
919
					if ( ! $connection->is_connected() ) {
920
						if ( ! doing_action( 'jetpack_site_registered' ) ) {
921
							WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
922
							return;
923
						}
924
					}
925
926
					$status = new Status();
927
928
					if ( $status->is_offline_mode() ) {
929
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in offline mode.', 'jetpack' ) );
930
						return;
931
					}
932
					if ( $status->is_staging_site() ) {
933
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in staging mode.', 'jetpack' ) );
934
						return;
935
					}
936
				}
937
				// Get the original settings so that we can restore them later
938
				$original_settings = Settings::get_settings();
939
940
				// Initialize sync settigns so we can sync as quickly as possible
941
				$sync_settings = wp_parse_args(
942
					array_intersect_key( $assoc_args, Settings::$valid_settings ),
943
					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...
944
						'sync_wait_time'           => 0,
945
						'enqueue_wait_time'        => 0,
946
						'queue_max_writes_sec'     => 10000,
947
						'max_queue_size_full_sync' => 100000,
948
						'full_sync_send_duration'  => HOUR_IN_SECONDS,
949
					)
950
				);
951
				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 941 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...
952
953
				// Convert comma-delimited string of modules to an array
954 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
955
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
956
957
					// Convert the array so that the keys are the module name and the value is true to indicate
958
					// that we want to sync the module
959
					$modules = array_map( '__return_true', array_flip( $modules ) );
960
				}
961
962 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
963
					if (
964
						'users' === $module_name &&
965
						isset( $assoc_args[ $module_name ] ) &&
966
						'initial' === $assoc_args[ $module_name ]
967
					) {
968
						$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...
969
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
970
						$ids = explode( ',', $assoc_args[ $module_name ] );
971
						if ( count( $ids ) > 0 ) {
972
							$modules[ $module_name ] = $ids;
973
						}
974
					}
975
				}
976
977
				if ( empty( $modules ) ) {
978
					$modules = null;
979
				}
980
981
				// Kick off a full sync
982
				if ( Actions::do_full_sync( $modules ) ) {
983 View Code Duplication
					if ( $modules ) {
984
						/* translators: %s is a comma separated list of Jetpack modules */
985
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
986
					} else {
987
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
988
					}
989 View Code Duplication
				} else {
990
991
					// Reset sync settings to original.
992
					Settings::update_settings( $original_settings );
993
994
					if ( $modules ) {
995
						/* translators: %s is a comma separated list of Jetpack modules */
996
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
997
					} else {
998
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
999
					}
1000
				}
1001
1002
				// Keep sending to WPCOM until there's nothing to send
1003
				$i = 1;
1004
				do {
1005
					$result = Actions::$sender->do_full_sync();
1006
					if ( is_wp_error( $result ) ) {
1007
						$queue_empty_error = ( 'empty_queue_full_sync' == $result->get_error_code() );
1008
						if ( ! $queue_empty_error || ( $queue_empty_error && ( 1 == $i ) ) ) {
1009
							/* translators: %s is an error code  */
1010
							WP_CLI::error( sprintf( __( 'Sync errored with code: %s', 'jetpack' ), $result->get_error_code() ) );
1011
						}
1012
					} else {
1013
						if ( 1 == $i ) {
1014
							WP_CLI::log( __( 'Sent data to WordPress.com', 'jetpack' ) );
1015
						} else {
1016
							WP_CLI::log( __( 'Sent more data to WordPress.com', 'jetpack' ) );
1017
						}
1018
1019
						// Immediate Full Sync does not wait for WP.com to process data so we need to enforce a wait.
1020
						if ( false !== strpos( get_class( Modules::get_module( 'full-sync' ) ), 'Full_Sync_Immediately' ) ) {
1021
							sleep( 15 );
1022
						}
1023
					}
1024
					$i++;
1025
				} while ( $result && ! is_wp_error( $result ) );
1026
1027
				// Reset sync settings to original.
1028
				Settings::update_settings( $original_settings );
1029
1030
				WP_CLI::success( __( 'Finished syncing to WordPress.com', 'jetpack' ) );
1031
				break;
1032
		}
1033
	}
1034
1035
	/**
1036
	 * List the contents of a specific Jetpack sync queue.
1037
	 *
1038
	 * ## OPTIONS
1039
	 *
1040
	 * peek : List the 100 front-most items on the queue.
1041
	 *
1042
	 * ## EXAMPLES
1043
	 *
1044
	 * wp jetpack sync_queue full_sync peek
1045
	 *
1046
	 * @synopsis <incremental|full_sync> <peek>
1047
	 */
1048
	public function sync_queue( $args, $assoc_args ) {
1049
		if ( ! Actions::sync_allowed() ) {
1050
			WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site.', 'jetpack' ) );
1051
		}
1052
1053
		$queue_name = isset( $args[0] ) ? $args[0] : 'sync';
1054
		$action     = isset( $args[1] ) ? $args[1] : 'peek';
1055
1056
		// We map the queue name that way we can support more friendly queue names in the commands, but still use
1057
		// the queue name that the code expects.
1058
		$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...
1059
			'incremental' => 'sync',
1060
			'full'        => 'full_sync',
1061
		);
1062
		$mapped_queue_name = isset( $queue_name_map[ $queue_name ] ) ? $queue_name_map[ $queue_name ] : $queue_name;
1063
1064
		switch ( $action ) {
1065
			case 'peek':
1066
				$queue = new Queue( $mapped_queue_name );
1067
				$items = $queue->peek( 100 );
1068
1069
				if ( empty( $items ) ) {
1070
					/* translators: %s is the name of the queue, either 'incremental' or 'full' */
1071
					WP_CLI::log( sprintf( __( 'Nothing is in the queue: %s', 'jetpack' ), $queue_name ) );
1072
				} else {
1073
					$collection = array();
1074
					foreach ( $items as $item ) {
1075
						$collection[] = array(
1076
							'action'          => $item[0],
1077
							'args'            => json_encode( $item[1] ),
1078
							'current_user_id' => $item[2],
1079
							'microtime'       => $item[3],
1080
							'importing'       => (string) $item[4],
1081
						);
1082
					}
1083
					WP_CLI\Utils\format_items(
1084
						'table',
1085
						$collection,
1086
						array(
1087
							'action',
1088
							'args',
1089
							'current_user_id',
1090
							'microtime',
1091
							'importing',
1092
						)
1093
					);
1094
				}
1095
				break;
1096
		}
1097
	}
1098
1099
	/**
1100
	 * Cancel's the current Jetpack plan granted by this partner, if applicable
1101
	 *
1102
	 * Returns success or error JSON
1103
	 *
1104
	 * <token_json>
1105
	 * : JSON blob of WPCOM API token
1106
	 *  [--partner_tracking_id=<partner_tracking_id>]
1107
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1108
	 *
1109
	 *  * @synopsis <token_json> [--partner_tracking_id=<partner_tracking_id>]
1110
	 */
1111
	public function partner_cancel( $args, $named_args ) {
1112
		list( $token_json ) = $args;
1113
1114 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1115
			/* translators: %s is the invalid JSON string */
1116
			$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...
1117
		}
1118
1119
		if ( isset( $token->error ) ) {
1120
			$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...
1121
		}
1122
1123
		if ( ! isset( $token->access_token ) ) {
1124
			$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...
1125
		}
1126
1127
		if ( Identity_Crisis::validate_sync_error_idc_option() ) {
1128
			$this->partner_provision_error(
1129
				new WP_Error(
1130
					'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...
1131
					esc_html__( 'Can not cancel a plan while in safe mode. See: https://jetpack.com/support/safe-mode/', 'jetpack' )
1132
				)
1133
			);
1134
		}
1135
1136
		$site_identifier = Jetpack_Options::get_option( 'id' );
1137
1138
		if ( ! $site_identifier ) {
1139
			$status          = new Status();
1140
			$site_identifier = $status->get_site_suffix();
1141
		}
1142
1143
		$request = array(
1144
			'headers' => array(
1145
				'Authorization' => 'Bearer ' . $token->access_token,
1146
				'Host'          => 'public-api.wordpress.com',
1147
			),
1148
			'timeout' => 60,
1149
			'method'  => 'POST',
1150
		);
1151
1152
		$url = sprintf( '%s/rest/v1.3/jpphp/%s/partner-cancel', $this->get_api_host(), $site_identifier );
1153 View Code Duplication
		if ( ! empty( $named_args ) && ! empty( $named_args['partner_tracking_id'] ) ) {
1154
			$url = esc_url_raw( add_query_arg( 'partner_tracking_id', $named_args['partner_tracking_id'], $url ) );
1155
		}
1156
1157
		$result = Client::_wp_remote_request( $url, $request );
1158
1159
		Jetpack_Options::delete_option( 'onboarding' );
1160
1161
		if ( is_wp_error( $result ) ) {
1162
			$this->partner_provision_error( $result );
1163
		}
1164
1165
		WP_CLI::log( wp_remote_retrieve_body( $result ) );
1166
	}
1167
1168
	/**
1169
	 * Provision a site using a Jetpack Partner license
1170
	 *
1171
	 * Returns JSON blob
1172
	 *
1173
	 * ## OPTIONS
1174
	 *
1175
	 * <token_json>
1176
	 * : JSON blob of WPCOM API token
1177
	 * [--plan=<plan_name>]
1178
	 * : Slug of the requested plan, e.g. premium
1179
	 * [--wpcom_user_id=<user_id>]
1180
	 * : WordPress.com ID of user to connect as (must be whitelisted against partner key)
1181
	 * [--wpcom_user_email=<wpcom_user_email>]
1182
	 * : Override the email we send to WordPress.com for registration
1183
	 * [--onboarding=<onboarding>]
1184
	 * : Guide the user through an onboarding wizard
1185
	 * [--force_register=<register>]
1186
	 * : Whether to force a site to register
1187
	 * [--force_connect=<force_connect>]
1188
	 * : Force JPS to not reuse existing credentials
1189
	 * [--home_url=<home_url>]
1190
	 * : Overrides the home option via the home_url filter, or the WP_HOME constant
1191
	 * [--site_url=<site_url>]
1192
	 * : Overrides the siteurl option via the site_url filter, or the WP_SITEURL constant
1193
	 * [--partner_tracking_id=<partner_tracking_id>]
1194
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1195
	 *
1196
	 * ## EXAMPLES
1197
	 *
1198
	 *     $ wp jetpack partner_provision '{ some: "json" }' premium 1
1199
	 *     { success: true }
1200
	 *
1201
	 * @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>]
1202
	 */
1203
	public function partner_provision( $args, $named_args ) {
1204
		list( $token_json ) = $args;
1205
1206 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1207
			/* translators: %s is the invalid JSON string */
1208
			$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...
1209
		}
1210
1211
		if ( isset( $token->error ) ) {
1212
			$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...
1213
				? $token->message
1214
				: '';
1215
			$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...
1216
		}
1217
1218
		if ( ! isset( $token->access_token ) ) {
1219
			$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...
1220
		}
1221
1222
		require_once JETPACK__PLUGIN_DIR . '_inc/class.jetpack-provision.php';
1223
1224
		$body_json = Jetpack_Provision::partner_provision( $token->access_token, $named_args );
1225
1226
		if ( is_wp_error( $body_json ) ) {
1227
			error_log(
1228
				json_encode(
1229
					array(
1230
						'success'       => false,
1231
						'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...
1232
						'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...
1233
					)
1234
				)
1235
			);
1236
			exit( 1 );
1237
		}
1238
1239
		WP_CLI::log( json_encode( $body_json ) );
1240
	}
1241
1242
	/**
1243
	 * Manages your Jetpack sitemap
1244
	 *
1245
	 * ## OPTIONS
1246
	 *
1247
	 * rebuild : Rebuild all sitemaps
1248
	 * --purge : if set, will remove all existing sitemap data before rebuilding
1249
	 *
1250
	 * ## EXAMPLES
1251
	 *
1252
	 * wp jetpack sitemap rebuild
1253
	 *
1254
	 * @subcommand sitemap
1255
	 * @synopsis <rebuild> [--purge]
1256
	 */
1257
	public function sitemap( $args, $assoc_args ) {
1258
		if ( ! Jetpack::is_connection_ready() ) {
1259
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1260
		}
1261
		if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1262
			WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1263
		}
1264
		if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1265
			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' ) );
1266
		}
1267
1268
		if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1269
			$librarian = new Jetpack_Sitemap_Librarian();
1270
			$librarian->delete_all_stored_sitemap_data();
1271
		}
1272
1273
		$sitemap_builder = new Jetpack_Sitemap_Builder();
1274
		$sitemap_builder->update_sitemap();
1275
	}
1276
1277
	/**
1278
	 * Allows authorizing a user via the command line and will activate
1279
	 *
1280
	 * ## EXAMPLES
1281
	 *
1282
	 * wp jetpack authorize_user --token=123456789abcdef
1283
	 *
1284
	 * @synopsis --token=<value>
1285
	 */
1286
	public function authorize_user( $args, $named_args ) {
1287
		if ( ! is_user_logged_in() ) {
1288
			WP_CLI::error( __( 'Please select a user to authorize via the --user global argument.', 'jetpack' ) );
1289
		}
1290
1291
		if ( empty( $named_args['token'] ) ) {
1292
			WP_CLI::error( __( 'A non-empty token argument must be passed.', 'jetpack' ) );
1293
		}
1294
1295
		$is_connection_owner = ! Jetpack::connection()->has_connected_owner();
1296
		$current_user_id     = get_current_user_id();
1297
1298
		( new Tokens() )->update_user_token( $current_user_id, sprintf( '%s.%d', $named_args['token'], $current_user_id ), $is_connection_owner );
1299
1300
		WP_CLI::log( wp_json_encode( $named_args ) );
1301
1302
		if ( $is_connection_owner ) {
1303
			/**
1304
			 * Auto-enable SSO module for new Jetpack Start connections
1305
			*
1306
			* @since 5.0.0
1307
			*
1308
			* @param bool $enable_sso Whether to enable the SSO module. Default to true.
1309
			*/
1310
			$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
1311
			Jetpack::handle_post_authorization_actions( $enable_sso, false );
1312
1313
			/* translators: %d is a user ID */
1314
			WP_CLI::success( sprintf( __( 'Authorized %d and activated default modules.', 'jetpack' ), $current_user_id ) );
1315
		} else {
1316
			/* translators: %d is a user ID */
1317
			WP_CLI::success( sprintf( __( 'Authorized %d.', 'jetpack' ), $current_user_id ) );
1318
		}
1319
	}
1320
1321
	/**
1322
	 * Allows calling a WordPress.com API endpoint using the current blog's token.
1323
	 *
1324
	 * ## OPTIONS
1325
	 * --resource=<resource>
1326
	 * : The resource to call with the current blog's token, where `%d` represents the current blog's ID.
1327
	 *
1328
	 * [--api_version=<api_version>]
1329
	 * : The API version to query against.
1330
	 *
1331
	 * [--base_api_path=<base_api_path>]
1332
	 * : The base API path to query.
1333
	 * ---
1334
	 * default: rest
1335
	 * ---
1336
	 *
1337
	 * [--body=<body>]
1338
	 * : A JSON encoded string representing arguments to send in the body.
1339
	 *
1340
	 * [--field=<value>]
1341
	 * : Any number of arguments that should be passed to the resource.
1342
	 *
1343
	 * [--pretty]
1344
	 * : Will pretty print the results of a successful API call.
1345
	 *
1346
	 * [--strip-success]
1347
	 * : Will remove the green success label from successful API calls.
1348
	 *
1349
	 * ## EXAMPLES
1350
	 *
1351
	 * wp jetpack call_api --resource='/sites/%d'
1352
	 */
1353
	public function call_api( $args, $named_args ) {
1354
		if ( ! Jetpack::is_connection_ready() ) {
1355
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1356
		}
1357
1358
		$consumed_args = array(
1359
			'resource',
1360
			'api_version',
1361
			'base_api_path',
1362
			'body',
1363
			'pretty',
1364
		);
1365
1366
		// Get args that should be passed to resource.
1367
		$other_args = array_diff_key( $named_args, array_flip( $consumed_args ) );
1368
1369
		$decoded_body = ! empty( $named_args['body'] )
1370
			? json_decode( $named_args['body'], true )
1371
			: false;
1372
1373
		$resource_url = ( false === strpos( $named_args['resource'], '%d' ) )
1374
			? $named_args['resource']
1375
			: sprintf( $named_args['resource'], Jetpack_Options::get_option( 'id' ) );
1376
1377
		$response = Client::wpcom_json_api_request_as_blog(
1378
			$resource_url,
1379
			empty( $named_args['api_version'] ) ? Client::WPCOM_JSON_API_VERSION : $named_args['api_version'],
1380
			$other_args,
1381
			empty( $decoded_body ) ? null : $decoded_body,
1382
			empty( $named_args['base_api_path'] ) ? 'rest' : $named_args['base_api_path']
1383
		);
1384
1385
		if ( is_wp_error( $response ) ) {
1386
			WP_CLI::error(
1387
				sprintf(
1388
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an error code, %3$s is an error message. */
1389
					__( 'Request to %1$s returned an error: (%2$d) %3$s.', 'jetpack' ),
1390
					$resource_url,
1391
					$response->get_error_code(),
1392
					$response->get_error_message()
1393
				)
1394
			);
1395
		}
1396
1397
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1398
			WP_CLI::error(
1399
				sprintf(
1400
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an HTTP status code. */
1401
					__( 'Request to %1$s returned a non-200 response code: %2$d.', 'jetpack' ),
1402
					$resource_url,
1403
					wp_remote_retrieve_response_code( $response )
1404
				)
1405
			);
1406
		}
1407
1408
		$output = wp_remote_retrieve_body( $response );
1409
		if ( isset( $named_args['pretty'] ) ) {
1410
			$decoded_output = json_decode( $output );
1411
			if ( $decoded_output ) {
1412
				$output = wp_json_encode( $decoded_output, JSON_PRETTY_PRINT );
1413
			}
1414
		}
1415
1416
		if ( isset( $named_args['strip-success'] ) ) {
1417
			WP_CLI::log( $output );
1418
			WP_CLI::halt( 0 );
1419
		}
1420
1421
		WP_CLI::success( $output );
1422
	}
1423
1424
	/**
1425
	 * Allows uploading SSH Credentials to the current site for backups, restores, and security scanning.
1426
	 *
1427
	 * ## OPTIONS
1428
	 *
1429
	 * [--host=<host>]
1430
	 * : The SSH server's address.
1431
	 *
1432
	 * [--ssh-user=<user>]
1433
	 * : The username to use to log in to the SSH server.
1434
	 *
1435
	 * [--pass=<pass>]
1436
	 * : The password used to log in, if using a password. (optional)
1437
	 *
1438
	 * [--kpri=<kpri>]
1439
	 * : The private key used to log in, if using a private key. (optional)
1440
	 *
1441
	 * [--pretty]
1442
	 * : Will pretty print the results of a successful API call. (optional)
1443
	 *
1444
	 * [--strip-success]
1445
	 * : Will remove the green success label from successful API calls. (optional)
1446
	 *
1447
	 * ## EXAMPLES
1448
	 *
1449
	 * wp jetpack upload_ssh_creds --host=example.com --ssh-user=example --pass=password
1450
	 * wp jetpack updload_ssh_creds --host=example.com --ssh-user=example --kpri=key
1451
	 */
1452
	public function upload_ssh_creds( $args, $named_args ) {
1453
		if ( ! Jetpack::is_connection_ready() ) {
1454
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1455
		}
1456
1457
		$required_args = array(
1458
			'host',
1459
			'ssh-user',
1460
		);
1461
1462
		foreach ( $required_args as $arg ) {
1463
			if ( empty( $named_args[ $arg ] ) ) {
1464
				WP_CLI::error(
1465
					sprintf(
1466
						/* translators: %s is a slug, such as 'host'. */
1467
						__( '`%s` cannot be empty.', 'jetpack' ),
1468
						$arg
1469
					)
1470
				);
1471
			}
1472
		}
1473
1474
		if ( empty( $named_args['pass'] ) && empty( $named_args['kpri'] ) ) {
1475
			WP_CLI::error( __( 'Both `pass` and `kpri` fields cannot be blank.', 'jetpack' ) );
1476
		}
1477
1478
		$values = array(
1479
			'credentials' => array(
1480
				'site_url' => get_site_url(),
1481
				'abspath'  => ABSPATH,
1482
				'protocol' => 'ssh',
1483
				'port'     => 22,
1484
				'role'     => 'main',
1485
				'host'     => $named_args['host'],
1486
				'user'     => $named_args['ssh-user'],
1487
				'pass'     => empty( $named_args['pass'] ) ? '' : $named_args['pass'],
1488
				'kpri'     => empty( $named_args['kpri'] ) ? '' : $named_args['kpri'],
1489
			),
1490
		);
1491
1492
		$named_args = wp_parse_args(
1493
			array(
1494
				'resource'    => '/activity-log/%d/update-credentials',
1495
				'method'      => 'POST',
1496
				'api_version' => '1.1',
1497
				'body'        => wp_json_encode( $values ),
1498
				'timeout'     => 30,
1499
			),
1500
			$named_args
1501
		);
1502
1503
		self::call_api( $args, $named_args );
1504
	}
1505
1506
	/**
1507
	 * API wrapper for getting stats from the WordPress.com API for the current site.
1508
	 *
1509
	 * ## OPTIONS
1510
	 *
1511
	 * [--quantity=<quantity>]
1512
	 * : The number of units to include.
1513
	 * ---
1514
	 * default: 30
1515
	 * ---
1516
	 *
1517
	 * [--period=<period>]
1518
	 * : The unit of time to query stats for.
1519
	 * ---
1520
	 * default: day
1521
	 * options:
1522
	 *  - day
1523
	 *  - week
1524
	 *  - month
1525
	 *  - year
1526
	 * ---
1527
	 *
1528
	 * [--date=<date>]
1529
	 * : The latest date to return stats for. Ex. - 2018-01-01.
1530
	 *
1531
	 * [--pretty]
1532
	 * : Will pretty print the results of a successful API call.
1533
	 *
1534
	 * [--strip-success]
1535
	 * : Will remove the green success label from successful API calls.
1536
	 *
1537
	 * ## EXAMPLES
1538
	 *
1539
	 * wp jetpack get_stats
1540
	 */
1541
	public function get_stats( $args, $named_args ) {
1542
		$selected_args = array_intersect_key(
1543
			$named_args,
1544
			array_flip(
1545
				array(
1546
					'quantity',
1547
					'date',
1548
				)
1549
			)
1550
		);
1551
1552
		// The API expects unit, but period seems to be more correct.
1553
		$selected_args['unit'] = $named_args['period'];
1554
1555
		$command = sprintf(
1556
			'jetpack call_api --resource=/sites/%d/stats/%s',
1557
			Jetpack_Options::get_option( 'id' ),
1558
			add_query_arg( $selected_args, 'visits' )
1559
		);
1560
1561
		if ( isset( $named_args['pretty'] ) ) {
1562
			$command .= ' --pretty';
1563
		}
1564
1565
		if ( isset( $named_args['strip-success'] ) ) {
1566
			$command .= ' --strip-success';
1567
		}
1568
1569
		WP_CLI::runcommand(
1570
			$command,
1571
			array(
1572
				'launch' => false, // Use the current process.
1573
			)
1574
		);
1575
	}
1576
1577
	/**
1578
	 * Allows management of publicize connections.
1579
	 *
1580
	 * ## OPTIONS
1581
	 *
1582
	 * <list|disconnect>
1583
	 * : The action to perform.
1584
	 * ---
1585
	 * options:
1586
	 *   - list
1587
	 *   - disconnect
1588
	 * ---
1589
	 *
1590
	 * [<identifier>]
1591
	 * : The connection ID or service to perform an action on.
1592
	 *
1593
	 * [--format=<format>]
1594
	 * : Allows overriding the output of the command when listing connections.
1595
	 * ---
1596
	 * default: table
1597
	 * options:
1598
	 *   - table
1599
	 *   - json
1600
	 *   - csv
1601
	 *   - yaml
1602
	 *   - ids
1603
	 *   - count
1604
	 * ---
1605
	 *
1606
	 * ## EXAMPLES
1607
	 *
1608
	 *     # List all publicize connections.
1609
	 *     $ wp jetpack publicize list
1610
	 *
1611
	 *     # List publicize connections for a given service.
1612
	 *     $ wp jetpack publicize list twitter
1613
	 *
1614
	 *     # List all publicize connections for a given user.
1615
	 *     $ wp --user=1 jetpack publicize list
1616
	 *
1617
	 *     # List all publicize connections for a given user and service.
1618
	 *     $ wp --user=1 jetpack publicize list twitter
1619
	 *
1620
	 *     # Display details for a given connection.
1621
	 *     $ wp jetpack publicize list 123456
1622
	 *
1623
	 *     # Diconnection a given connection.
1624
	 *     $ wp jetpack publicize disconnect 123456
1625
	 *
1626
	 *     # Disconnect all connections.
1627
	 *     $ wp jetpack publicize disconnect all
1628
	 *
1629
	 *     # Disconnect all connections for a given service.
1630
	 *     $ wp jetpack publicize disconnect twitter
1631
	 */
1632
	public function publicize( $args, $named_args ) {
1633
		if ( ! Jetpack::connection()->has_connected_owner() ) {
1634
			WP_CLI::error( __( 'Publicize requires a user-level connection to WordPress.com', 'jetpack' ) );
1635
		}
1636
1637
		if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1638
			WP_CLI::error( __( 'The publicize module is not active.', 'jetpack' ) );
1639
		}
1640
1641
		if ( ( new Status() )->is_offline_mode() ) {
1642
			if (
1643
				! defined( 'JETPACK_DEV_DEBUG' ) &&
1644
				! has_filter( 'jetpack_development_mode' ) &&
1645
				! has_filter( 'jetpack_offline_mode' ) &&
1646
				false === strpos( site_url(), '.' )
1647
			) {
1648
				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' ) );
1649
			}
1650
1651
			WP_CLI::error( __( 'Jetpack is currently in offline mode, so the publicize module will not load.', 'jetpack' ) );
1652
		}
1653
1654
		if ( ! class_exists( 'Publicize' ) ) {
1655
			WP_CLI::error( __( 'The publicize module is not loaded.', 'jetpack' ) );
1656
		}
1657
1658
		$action        = $args[0];
1659
		$publicize     = new Publicize();
1660
		$identifier    = ! empty( $args[1] ) ? $args[1] : false;
1661
		$services      = array_keys( $publicize->get_services() );
1662
		$id_is_service = in_array( $identifier, $services, true );
1663
1664
		switch ( $action ) {
1665
			case 'list':
1666
				$connections_to_return = array();
1667
1668
				// For the CLI command, let's return all connections when a user isn't specified. This
1669
				// differs from the logic in the Publicize class.
1670
				$option_connections = is_user_logged_in()
1671
					? (array) $publicize->get_all_connections_for_user()
1672
					: (array) $publicize->get_all_connections();
1673
1674
				foreach ( $option_connections as $service_name => $connections ) {
1675
					foreach ( (array) $connections as $id => $connection ) {
1676
						$connection['id']        = $id;
1677
						$connection['service']   = $service_name;
1678
						$connections_to_return[] = $connection;
1679
					}
1680
				}
1681
1682
				if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1683
					$temp_connections      = $connections_to_return;
1684
					$connections_to_return = array();
1685
1686
					foreach ( $temp_connections as $connection ) {
1687
						if ( $identifier === $connection['service'] ) {
1688
							$connections_to_return[] = $connection;
1689
						}
1690
					}
1691
				}
1692
1693
				if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1694
					$connections_to_return = wp_list_filter( $connections_to_return, array( 'id' => $identifier ) );
1695
				}
1696
1697
				$expected_keys = array(
1698
					'id',
1699
					'service',
1700
					'user_id',
1701
					'provider',
1702
					'issued',
1703
					'expires',
1704
					'external_id',
1705
					'external_name',
1706
					'external_display',
1707
					'type',
1708
					'connection_data',
1709
				);
1710
1711
				// Somehow, a test site ended up in a state where $connections_to_return looked like:
1712
				// array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1713
				// This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1714
				// to minimize future issues, this nested loop will remove any connections that don't contain
1715
				// any keys that we expect.
1716
				foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1717
					foreach ( $expected_keys as $expected_key ) {
1718
						if ( ! isset( $connection[ $expected_key ] ) ) {
1719
							unset( $connections_to_return[ $connection_key ] );
1720
							continue;
1721
						}
1722
					}
1723
				}
1724
1725
				if ( empty( $connections_to_return ) ) {
1726
					return false;
1727
				}
1728
1729
				WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1730
				break; // list.
1731
			case 'disconnect':
1732
				if ( ! $identifier ) {
1733
					WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1734
				}
1735
1736
				// If the connection ID is 'all' then delete all connections. If the connection ID
1737
				// matches a service, delete all connections for that service.
1738
				if ( 'all' === $identifier || $id_is_service ) {
1739
					if ( 'all' === $identifier ) {
1740
						WP_CLI::log( __( "You're about to delete all publicize connections.", 'jetpack' ) );
1741
					} else {
1742
						/* translators: %s is a lowercase string for a social network. */
1743
						WP_CLI::log( sprintf( __( "You're about to delete all publicize connections to %s.", 'jetpack' ), $identifier ) );
1744
					}
1745
1746
					jetpack_cli_are_you_sure();
1747
1748
					$connections = array();
1749
					$service     = $identifier;
1750
1751
					$option_connections = is_user_logged_in()
1752
						? (array) $publicize->get_all_connections_for_user()
1753
						: (array) $publicize->get_all_connections();
1754
1755
					if ( 'all' === $service ) {
1756
						foreach ( (array) $option_connections as $service_name => $service_connections ) {
1757
							foreach ( $service_connections as $id => $connection ) {
1758
								$connections[ $id ] = $connection;
1759
							}
1760
						}
1761
					} elseif ( ! empty( $option_connections[ $service ] ) ) {
1762
						$connections = $option_connections[ $service ];
1763
					}
1764
1765
					if ( ! empty( $connections ) ) {
1766
						$count    = count( $connections );
1767
						$progress = \WP_CLI\Utils\make_progress_bar(
1768
							/* translators: %s is a lowercase string for a social network. */
1769
							sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1770
							$count
1771
						);
1772
1773
						foreach ( $connections as $id => $connection ) {
1774
							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...
1775
								WP_CLI::error(
1776
									sprintf(
1777
										/* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1778
										__( 'Publicize connection %d could not be disconnected', 'jetpack' ),
1779
										$id
1780
									)
1781
								);
1782
							}
1783
1784
							$progress->tick();
1785
						}
1786
1787
						$progress->finish();
1788
1789
						if ( 'all' === $service ) {
1790
							WP_CLI::success( __( 'All publicize connections were successfully disconnected.', 'jetpack' ) );
1791
						} else {
1792
							/* translators: %s is a lowercase string for a social network. */
1793
							WP_CLI::success( __( 'All publicize connections to %s were successfully disconnected.', 'jetpack' ), $service );
1794
						}
1795
					}
1796
				} else {
1797
					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...
1798
						/* translators: %d is a numeric ID. Example: 1234. */
1799
						WP_CLI::success( sprintf( __( 'Publicize connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1800
					} else {
1801
						/* translators: %d is a numeric ID. Example: 1234. */
1802
						WP_CLI::error( sprintf( __( 'Publicize connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1803
					}
1804
				}
1805
				break; // disconnect.
1806
		}
1807
	}
1808
1809
	private function get_api_host() {
1810
		$env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1811
		return $env_api_host ? 'https://' . $env_api_host : JETPACK__WPCOM_JSON_API_BASE;
1812
	}
1813
1814
	private function partner_provision_error( $error ) {
1815
		WP_CLI::log(
1816
			json_encode(
1817
				array(
1818
					'success'       => false,
1819
					'error_code'    => $error->get_error_code(),
1820
					'error_message' => $error->get_error_message(),
1821
				)
1822
			)
1823
		);
1824
		exit( 1 );
1825
	}
1826
1827
	/**
1828
	 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1829
	 *
1830
	 * ## TYPES
1831
	 *
1832
	 * 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.
1833
	 *
1834
	 * ## BLOCK TYPE OPTIONS
1835
	 *
1836
	 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1837
	 * 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.
1838
	 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1839
	 * --description: Allows to provide a text description of the block.
1840
	 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1841
	 * --variation: Allows to decide whether the block should be a production block, experimental, or beta. Defaults to Beta when arg not provided.
1842
	 *
1843
	 * ## BLOCK TYPE EXAMPLES
1844
	 *
1845
	 * wp jetpack scaffold block "Cool Block"
1846
	 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1847
	 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1848
	 * wp jetpack scaffold block "Jukebox" --variation="experimental"
1849
	 *
1850
	 * @subcommand scaffold block
1851
	 * @synopsis <type> <title> [--slug] [--description] [--keywords] [--variation]
1852
	 *
1853
	 * @param array $args       Positional parameters, when strings are passed, wrap them in quotes.
1854
	 * @param array $assoc_args Associative parameters like --slug="nice-block".
1855
	 */
1856
	public function scaffold( $args, $assoc_args ) {
1857
		// It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1858
		switch ( $args[0] ) {
1859
			case 'block':
1860
				$this->block( $args, $assoc_args );
1861
				break;
1862
			default:
1863
				/* translators: %s is the subcommand */
1864
				WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1865
				exit( 1 );
1866
		}
1867
	}
1868
1869
	/**
1870
	 * Creates the essential files in Jetpack to build a Gutenberg block.
1871
	 *
1872
	 * @param array $args       Positional parameters. Only one is used, that corresponds to the block title.
1873
	 * @param array $assoc_args Associative parameters defined in the scaffold() method.
1874
	 */
1875
	public function block( $args, $assoc_args ) {
1876 View Code Duplication
		if ( isset( $args[1] ) ) {
1877
			$title = ucwords( $args[1] );
1878
		} else {
1879
			WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
1880
			exit( 1 );
1881
		}
1882
1883
		$slug = isset( $assoc_args['slug'] )
1884
			? $assoc_args['slug']
1885
			: sanitize_title( $title );
1886
1887
		$variation_options = array( 'production', 'experimental', 'beta' );
1888
		$variation         = ( isset( $assoc_args['variation'] ) && in_array( $assoc_args['variation'], $variation_options, true ) )
1889
			? $assoc_args['variation']
1890
			: 'beta';
1891
1892
		if ( preg_match( '#^jetpack/#', $slug ) ) {
1893
			$slug = preg_replace( '#^jetpack/#', '', $slug );
1894
		}
1895
1896
		if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
1897
			WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
1898
		}
1899
1900
		global $wp_filesystem;
1901
		if ( ! WP_Filesystem() ) {
1902
			WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
1903
		}
1904
1905
		$path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
1906
1907
		if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
1908
			/* translators: %s is path to the conflicting block */
1909
			WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
1910
			exit( 1 );
1911
		}
1912
1913
		$wp_filesystem->mkdir( $path );
1914
1915
		$hasKeywords = isset( $assoc_args['keywords'] );
1916
1917
		$files = array(
1918
			"$path/$slug.php"     => $this->render_block_file(
1919
				'block-register-php',
1920
				array(
1921
					'slug'             => $slug,
1922
					'title'            => $title,
1923
					'underscoredSlug'  => str_replace( '-', '_', $slug ),
1924
					'underscoredTitle' => str_replace( ' ', '_', $title ),
1925
					'jetpackVersion'   => substr( JETPACK__VERSION, 0, strpos( JETPACK__VERSION, '.' ) ) . '.x',
1926
				)
1927
			),
1928
			"$path/index.js"      => $this->render_block_file(
1929
				'block-index-js',
1930
				array(
1931
					'slug'        => $slug,
1932
					'title'       => $title,
1933
					'description' => isset( $assoc_args['description'] )
1934
						? $assoc_args['description']
1935
						: $title,
1936
					'keywords'    => $hasKeywords
1937
					? array_map(
1938
						function( $keyword ) {
1939
								// Construction necessary for Mustache lists
1940
								return array( 'keyword' => trim( $keyword ) );
1941
						},
1942
						explode( ',', $assoc_args['keywords'], 3 )
1943
					)
1944
					: '',
1945
					'hasKeywords' => $hasKeywords,
1946
				)
1947
			),
1948
			"$path/editor.js"     => $this->render_block_file( 'block-editor-js' ),
1949
			"$path/editor.scss"   => $this->render_block_file(
1950
				'block-editor-scss',
1951
				array(
1952
					'slug'  => $slug,
1953
					'title' => $title,
1954
				)
1955
			),
1956
			"$path/edit.js"       => $this->render_block_file(
1957
				'block-edit-js',
1958
				array(
1959
					'title'     => $title,
1960
					'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
1961
				)
1962
			),
1963
			"$path/icon.js"       => $this->render_block_file( 'block-icon-js' ),
1964
			"$path/attributes.js" => $this->render_block_file( 'block-attributes-js' ),
1965
		);
1966
1967
		$files_written = array();
1968
1969
		foreach ( $files as $filename => $contents ) {
1970
			if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
1971
				$files_written[] = $filename;
1972
			} else {
1973
				/* translators: %s is a file name */
1974
				WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
1975
			}
1976
		}
1977
1978
		if ( empty( $files_written ) ) {
1979
			WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
1980
		} else {
1981
			// Load index.json and insert the slug of the new block in its block variation array.
1982
			$block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
1983
			$block_list      = $wp_filesystem->get_contents( $block_list_path );
1984
			if ( empty( $block_list ) ) {
1985
				/* translators: %s is the path to the file with the block list */
1986
				WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
1987
			} elseif ( false === stripos( $block_list, $slug ) ) {
1988
				$new_block_list                   = json_decode( $block_list );
1989
				$new_block_list->{ $variation }[] = $slug;
1990
1991
				// Format the JSON to match our coding standards.
1992
				$new_block_list_formatted = wp_json_encode( $new_block_list, JSON_PRETTY_PRINT ) . "\n";
1993
				$new_block_list_formatted = preg_replace_callback(
1994
					// Find all occurrences of multiples of 4 spaces a the start of the line.
1995
					'/^((?:    )+)/m',
1996
					function ( $matches ) {
1997
						// Replace each occurrence of 4 spaces with a tab character.
1998
						return str_repeat( "\t", substr_count( $matches[0], '    ' ) );
1999
					},
2000
					$new_block_list_formatted
2001
				);
2002
2003
				if ( ! $wp_filesystem->put_contents( $block_list_path, $new_block_list_formatted ) ) {
2004
					/* translators: %s is the path to the file with the block list */
2005
					WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
2006
				}
2007
			}
2008
2009
			if ( 'beta' === $variation || 'experimental' === $variation ) {
2010
				$block_constant = sprintf(
2011
					/* translators: the placeholder is a constant name */
2012
					esc_html__( 'To load the block, add the constant %1$s as true to your wp-config.php file', 'jetpack' ),
2013
					( 'beta' === $variation ? 'JETPACK_BETA_BLOCKS' : 'JETPACK_EXPERIMENTAL_BLOCKS' )
2014
				);
2015
			} else {
2016
				$block_constant = '';
2017
			}
2018
2019
			WP_CLI::success(
2020
				sprintf(
2021
					/* translators: the placeholders are a human readable title, and a series of words separated by dashes */
2022
					esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
2023
					"--------------------------------------------------------------------------------------------------------------------\n" .
2024
					/* translators: the placeholder is a directory path */
2025
					esc_html__( 'The files were created at %3$s', 'jetpack' ) . "\n" .
2026
					esc_html__( 'To start using the block, build the blocks with pnpm run build-extensions', 'jetpack' ) . "\n" .
2027
					/* translators: the placeholder is a file path */
2028
					esc_html__( 'The block slug has been added to the %4$s list at %5$s', 'jetpack' ) . "\n" .
2029
					'%6$s' . "\n" .
2030
					/* translators: the placeholder is a URL */
2031
					"\n" . esc_html__( 'Read more at %7$s', 'jetpack' ) . "\n",
2032
					$title,
2033
					$slug,
2034
					$path,
2035
					$variation,
2036
					$block_list_path,
2037
					$block_constant,
2038
					'https://github.com/Automattic/jetpack/blob/master/extensions/README.md#develop-new-blocks'
2039
				) . '--------------------------------------------------------------------------------------------------------------------'
2040
			);
2041
		}
2042
	}
2043
2044
	/**
2045
	 * Built the file replacing the placeholders in the template with the data supplied.
2046
	 *
2047
	 * @param string $template
2048
	 * @param array  $data
2049
	 *
2050
	 * @return string mixed
2051
	 */
2052
	private static function render_block_file( $template, $data = array() ) {
2053
		return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
2054
	}
2055
}
2056
2057
/*
2058
 * Standard "ask for permission to continue" function.
2059
 * If action cancelled, ask if they need help.
2060
 *
2061
 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
2062
 *
2063
 * @param $flagged   bool   false = normal option | true = flagged by get_jetpack_options_for_reset()
2064
 * @param $error_msg string (optional)
2065
 */
2066
function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
2067
	$cli = new Jetpack_CLI();
2068
2069
	// Default cancellation message
2070
	if ( ! $error_msg ) {
2071
		$error_msg =
2072
			__( 'Action cancelled. Have a question?', 'jetpack' )
2073
			. ' '
2074
			. $cli->green_open
2075
			. 'jetpack.com/support'
2076
			. $cli->color_close;
2077
	}
2078
2079
	if ( ! $flagged ) {
2080
		$prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
2081
	} else {
2082
		$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' );
2083
	}
2084
2085
	WP_CLI::line( $prompt_message );
2086
	$handle = fopen( 'php://stdin', 'r' );
2087
	$line   = fgets( $handle );
2088
	if ( 'yes' != trim( $line ) ) {
2089
		WP_CLI::error( $error_msg );
2090
	}
2091
}
2092