Completed
Push — add/debugger-package ( efda91 )
by
unknown
15:59 queued 06:31
created

class.jetpack-cli.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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