Completed
Push — fix/12791-track-upgrade ( 62035b...97a755 )
by
unknown
26:48 queued 17:41
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\Sync\Actions;
7
use Automattic\Jetpack\Sync\Listener;
8
use Automattic\Jetpack\Sync\Queue;
9
10
/**
11
 * Control your local Jetpack installation.
12
 *
13
 * Minimum PHP requirement for WP-CLI is PHP 5.3, so ignore PHP 5.2 compatibility issues.
14
 * @phpcs:disable PHPCompatibility.PHP.NewLanguageConstructs.t_ns_separatorFound
15
 */
16
class Jetpack_CLI extends WP_CLI_Command {
17
	// Aesthetics
18
	public $green_open  = "\033[32m";
19
	public $red_open    = "\033[31m";
20
	public $yellow_open = "\033[33m";
21
	public $color_close = "\033[0m";
22
23
	/**
24
	 * Get Jetpack Details
25
	 *
26
	 * ## OPTIONS
27
	 *
28
	 * empty: Leave it empty for basic stats
29
	 *
30
	 * full: View full stats.  It's the data from the heartbeat
31
	 *
32
	 * ## EXAMPLES
33
	 *
34
	 * wp jetpack status
35
	 * wp jetpack status full
36
	 *
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 ) {
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 View Code Duplication
		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::error( __( 'You cannot disconnect, without having first connected.', 'jetpack' ) );
189
		}
190
191
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
192
		if ( ! in_array( $action, array( 'blog', 'user', 'prompt' ) ) ) {
193
			/* translators: %s is a command like "prompt" */
194
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
195
		}
196
197
		if ( in_array( $action, array( 'user' ) ) ) {
198
			if ( isset( $args[1] ) ) {
199
				$user_id = $args[1];
200
				if ( ctype_digit( $user_id ) ) {
201
					$field = 'id';
202
					$user_id = (int) $user_id;
203
				} elseif ( is_email( $user_id ) ) {
204
					$field = 'email';
205
					$user_id = sanitize_user( $user_id, true );
206
				} else {
207
					$field = 'login';
208
					$user_id = sanitize_user( $user_id, true );
209
				}
210
				if ( ! $user = get_user_by( $field, $user_id ) ) {
211
					WP_CLI::error( __( 'Please specify a valid user.', 'jetpack' ) );
212
				}
213
			} else {
214
				WP_CLI::error( __( 'Please specify a user by either ID, username, or email.', 'jetpack' ) );
215
			}
216
		}
217
218
		switch ( $action ) {
219
			case 'blog':
220
				Jetpack::log( 'disconnect' );
221
				Jetpack::disconnect();
222
				WP_CLI::success( sprintf(
223
					/* translators: %s is the site URL */
224
					__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
225
					esc_url( get_site_url() )
226
				) );
227
				break;
228
			case 'user':
229
				if ( Jetpack::unlink_user( $user->ID ) ) {
230
					Jetpack::log( 'unlink', $user->ID );
231
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
232
				} else {
233
					/* translators: %s is a username */
234
					WP_CLI::error( sprintf( __( "User %s could not be disconnected. Are you sure they're connected currently?", 'jetpack' ), "{$user->login} <{$user->email}>" ) );
235
				}
236
				break;
237
			case 'prompt':
238
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
239
				break;
240
		}
241
	}
242
243
	/**
244
	 * Reset Jetpack options and settings to default
245
	 *
246
	 * ## OPTIONS
247
	 *
248
	 * modules: Resets modules to default state ( get_default_modules() )
249
	 *
250
	 * options: Resets all Jetpack options except:
251
	 *  - All private options (Blog token, user token, etc...)
252
	 *  - id (The Client ID/WP.com Blog ID of this site)
253
	 *  - master_user
254
	 *  - version
255
	 *  - activated
256
	 *
257
	 * ## EXAMPLES
258
	 *
259
	 * wp jetpack reset options
260
	 * wp jetpack reset modules
261
	 * wp jetpack reset sync-checksum --dry-run --offset=0
262
	 *
263
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
264
	 *
265
	 */
266
	public function reset( $args, $assoc_args ) {
267
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
268 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
269
			/* translators: %s is a command like "prompt" */
270
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
271
		}
272
273
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
274
275 View Code Duplication
		if ( $is_dry_run ) {
276
			WP_CLI::warning(
277
				__( "\nThis is a dry run.\n", 'jetpack' ) .
278
				__( "No actions will be taken.\n", 'jetpack' ) .
279
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
280
			);
281
		} else {
282
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
283
			jetpack_cli_are_you_sure();
284
		}
285
286
		switch ( $action ) {
287
			case 'options':
288
				$options_to_reset = Jetpack_Options::get_options_for_reset();
289
				// Reset the Jetpack options
290
				WP_CLI::line( sprintf(
291
					/* translators: %s is the site URL */
292
					__( "Resetting Jetpack Options for %s...\n", "jetpack" ),
293
					esc_url( get_site_url() )
294
				) );
295
				sleep(1); // Take a breath
296 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
297
					if ( ! $is_dry_run ) {
298
						Jetpack_Options::delete_option( $option_to_reset );
299
						usleep( 100000 );
300
					}
301
302
					/* translators: This is the result of an action. The option named %s was reset */
303
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
304
				}
305
306
				// Reset the WP options
307
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", "jetpack" ) );
308
				usleep( 500000 ); // Take a breath
309 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
310
					if ( ! $is_dry_run ) {
311
						delete_option( $option_to_reset );
312
						usleep( 100000 );
313
					}
314
					/* translators: This is the result of an action. The option named %s was reset */
315
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
316
				}
317
318
				// Reset to default modules
319
				WP_CLI::line( __( "Resetting default modules...\n", "jetpack" ) );
320
				usleep( 500000 ); // Take a breath
321
				$default_modules = Jetpack::get_default_modules();
322
				if ( ! $is_dry_run ) {
323
					Jetpack::update_active_modules( $default_modules );
324
				}
325
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
326
				break;
327 View Code Duplication
			case 'modules':
328
				if ( ! $is_dry_run ) {
329
					$default_modules = Jetpack::get_default_modules();
330
					Jetpack::update_active_modules( $default_modules );
331
				}
332
333
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
334
				break;
335
			case 'prompt':
336
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
337
				break;
338
			case 'sync-checksum':
339
				$option = 'jetpack_callables_sync_checksum';
340
341
				if ( is_multisite() ) {
342
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
343
344
					/*
345
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
346
					 * Offset can be used to paginate and try to clean up more sites.
347
					 */
348
					$sites       = get_sites( array( 'number' => 1000, 'offset' => $offset ) );
349
					$count_fixes = 0;
350
					foreach ( $sites as $site ) {
351
						switch_to_blog( $site->blog_id );
352
						$count = self::count_option( $option );
353
						if ( $count > 1 ) {
354
							if ( ! $is_dry_run ) {
355
								delete_option( $option );
356
							}
357
							WP_CLI::line(
358
								sprintf(
359
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
360
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
361
									$count,
362
									$option,
363
									"{$site->domain}{$site->path}"
364
								)
365
							);
366
							$count_fixes++;
367
							if ( ! $is_dry_run ) {
368
								/*
369
								 * We could be deleting a lot of options rows at the same time.
370
								 * Allow some time for replication to catch up.
371
								 */
372
								sleep( 3 );
373
							}
374
						}
375
376
						restore_current_blog();
377
					}
378
					if ( $count_fixes ) {
379
						WP_CLI::success(
380
							sprintf(
381
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
382
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
383
								$option,
384
								$count_fixes
385
							)
386
						);
387
					} else {
388
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
389
					}
390
					return;
391
				}
392
393
				$count = self::count_option( $option );
394
				if ( $count > 1 ) {
395
					if ( ! $is_dry_run ) {
396
						delete_option( $option );
397
					}
398
					WP_CLI::success(
399
						sprintf(
400
							/* translators: %1$d is a number, %2$s is the name of an option. */
401
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
402
							$count,
403
							$option
404
						)
405
					);
406
					return;
407
				}
408
409
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
410
				break;
411
412
		}
413
	}
414
415
	/**
416
	 * Return the number of times an option appears
417
	 * Normally an option would only appear 1 since the option key is supposed to be unique
418
	 * but if a site hasn't updated the DB schema then that would not be the case.
419
	 *
420
	 * @param string $option Option name.
421
	 *
422
	 * @return int
423
	 */
424
	private static function count_option( $option ) {
425
		global $wpdb;
426
		return (int) $wpdb->get_var(
427
			$wpdb->prepare(
428
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
429
				$option
430
			)
431
		);
432
433
	}
434
435
	/**
436
	 * Manage Jetpack Modules
437
	 *
438
	 * ## OPTIONS
439
	 *
440
	 * <list|activate|deactivate|toggle>
441
	 * : The action to take.
442
	 * ---
443
	 * default: list
444
	 * options:
445
	 *  - list
446
	 *  - activate
447
	 *  - deactivate
448
	 *  - toggle
449
	 * ---
450
	 *
451
	 * [<module_slug>]
452
	 * : The slug of the module to perform an action on.
453
	 *
454
	 * [--format=<format>]
455
	 * : Allows overriding the output of the command when listing modules.
456
	 * ---
457
	 * default: table
458
	 * options:
459
	 *  - table
460
	 *  - json
461
	 *  - csv
462
	 *  - yaml
463
	 *  - ids
464
	 *  - count
465
	 * ---
466
	 *
467
	 * ## EXAMPLES
468
	 *
469
	 * wp jetpack module list
470
	 * wp jetpack module list --format=json
471
	 * wp jetpack module activate stats
472
	 * wp jetpack module deactivate stats
473
	 * wp jetpack module toggle stats
474
	 * wp jetpack module activate all
475
	 * wp jetpack module deactivate all
476
	 */
477
	public function module( $args, $assoc_args ) {
478
		$action = isset( $args[0] ) ? $args[0] : 'list';
479
480
		if ( isset( $args[1] ) ) {
481
			$module_slug = $args[1];
482
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
483
				/* translators: %s is a module slug like "stats" */
484
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
485
			}
486
			if ( 'toggle' === $action ) {
487
				$action = Jetpack::is_module_active( $module_slug )
488
					? 'deactivate'
489
					: 'activate';
490
			}
491
			if ( 'all' === $args[1] ) {
492
				$action = ( 'deactivate' === $action )
493
					? 'deactivate_all'
494
					: 'activate_all';
495
			}
496
		} elseif ( 'list' !== $action ) {
497
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
498
			$action = 'list';
499
		}
500
501
		switch ( $action ) {
502
			case 'list':
503
				$modules_list = array();
504
				$modules      = Jetpack::get_available_modules();
505
				sort( $modules );
506
				foreach ( (array) $modules as $module_slug ) {
507
					if ( 'vaultpress' === $module_slug ) {
508
						continue;
509
					}
510
					$modules_list[] = array(
511
						'slug'   => $module_slug,
512
						'status' => Jetpack::is_module_active( $module_slug )
513
							? __( 'Active', 'jetpack' )
514
							: __( 'Inactive', 'jetpack' ),
515
					);
516
				}
517
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
518
				break;
519
			case 'activate':
520
				$module = Jetpack::get_module( $module_slug );
521
				Jetpack::log( 'activate', $module_slug );
522
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
523
					/* translators: %s is the name of a Jetpack module */
524
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
525
				} else {
526
					/* translators: %s is the name of a Jetpack module */
527
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
528
				}
529
				break;
530 View Code Duplication
			case 'activate_all':
531
				$modules = Jetpack::get_available_modules();
532
				Jetpack::update_active_modules( $modules );
533
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
534
				break;
535
			case 'deactivate':
536
				$module = Jetpack::get_module( $module_slug );
537
				Jetpack::log( 'deactivate', $module_slug );
538
				Jetpack::deactivate_module( $module_slug );
539
				/* translators: %s is the name of a Jetpack module */
540
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
541
				break;
542
			case 'deactivate_all':
543
				Jetpack::delete_active_modules();
544
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
545
				break;
546
			case 'toggle':
547
				// Will never happen, should have been handled above and changed to activate or deactivate.
548
				break;
549
		}
550
	}
551
552
	/**
553
	 * Manage Protect Settings
554
	 *
555
	 * ## OPTIONS
556
	 *
557
	 * whitelist: Whitelist an IP address.  You can also read or clear the whitelist.
558
	 *
559
	 *
560
	 * ## EXAMPLES
561
	 *
562
	 * wp jetpack protect whitelist <ip address>
563
	 * wp jetpack protect whitelist list
564
	 * wp jetpack protect whitelist clear
565
	 *
566
	 * @synopsis <whitelist> [<ip|ip_low-ip_high|list|clear>]
567
	 */
568
	public function protect( $args, $assoc_args ) {
569
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
570
		if ( ! in_array( $action, array( 'whitelist' ) ) ) {
571
			/* translators: %s is a command like "prompt" */
572
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
573
		}
574
		// Check if module is active
575
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
576
			/* translators: %s is a module name */
577
			WP_CLI::error( sprintf( _x( '%s is not active. You can activate it with "wp jetpack module activate %s"', '"wp jetpack module activate" is a command - do not translate', 'jetpack' ), __FUNCTION__, __FUNCTION__ ) );
578
		}
579
		if ( in_array( $action, array( 'whitelist' ) ) ) {
580
			if ( isset( $args[1] ) ) {
581
				$action = 'whitelist';
582
			} else {
583
				$action = 'prompt';
584
			}
585
		}
586
		switch ( $action ) {
587
			case 'whitelist':
588
				$whitelist         = array();
589
				$new_ip            = $args[1];
590
				$current_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
591
592
				// Build array of IPs that are already whitelisted.
593
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
594
				// low & high range params for jetpack_protect_ip_address_is_in_range();
595
				foreach( $current_whitelist as $whitelisted ) {
596
597
					// IP ranges
598
					if ( $whitelisted->range ) {
599
600
						// Is it already whitelisted?
601
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $whitelisted->range_low, $whitelisted->range_high ) ) {
602
							/* translators: %s is an IP address */
603
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
604
							break;
605
						}
606
						$whitelist[] = $whitelisted->range_low . " - " . $whitelisted->range_high;
607
608
					} else { // Individual IPs
609
610
						// Check if the IP is already whitelisted (single IP only)
611
						if ( $new_ip == $whitelisted->ip_address ) {
612
							/* translators: %s is an IP address */
613
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
614
							break;
615
						}
616
						$whitelist[] = $whitelisted->ip_address;
617
618
					}
619
				}
620
621
				/*
622
				 * List the whitelist
623
				 * Done here because it's easier to read the $whitelist array after it's been rebuilt
624
				 */
625
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
626
					if ( ! empty( $whitelist ) ) {
627
						WP_CLI::success( __( 'Here are your whitelisted IPs:', 'jetpack' ) );
628
						foreach ( $whitelist as $ip ) {
629
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) ) ;
630
						}
631
					} else {
632
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
633
					}
634
					break;
635
				}
636
637
				/*
638
				 * Clear the whitelist
639
				 */
640
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
641
					if ( ! empty( $whitelist ) ) {
642
						$whitelist = array();
643
						jetpack_protect_save_whitelist( $whitelist );
644
						WP_CLI::success( __( 'Cleared all whitelisted IPs', 'jetpack' ) );
645
					} else {
646
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
647
					}
648
					break;
649
				}
650
651
				// Append new IP to whitelist array
652
				array_push( $whitelist, $new_ip );
653
654
				// Save whitelist if there are no errors
655
				$result = jetpack_protect_save_whitelist( $whitelist );
656
				if ( is_wp_error( $result ) ) {
657
					WP_CLI::error( $result );
658
				}
659
660
				/* translators: %s is an IP address */
661
				WP_CLI::success( sprintf( __( '%s has been whitelisted.', 'jetpack' ), $new_ip ) );
662
				break;
663
			case 'prompt':
664
				WP_CLI::error(
665
					__( 'No command found.', 'jetpack' ) . "\n" .
666
					__( 'Please enter the IP address you want to whitelist.', 'jetpack' ) . "\n" .
667
					_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" .
668
					_x( "You can also 'list' or 'clear' the whitelist.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
669
				);
670
				break;
671
		}
672
	}
673
674
	/**
675
	 * Manage Jetpack Options
676
	 *
677
	 * ## OPTIONS
678
	 *
679
	 * list   : List all jetpack options and their values
680
	 * delete : Delete an option
681
	 *          - can only delete options that are white listed.
682
	 * update : update an option
683
	 *          - can only update option strings
684
	 * get    : get the value of an option
685
	 *
686
	 * ## EXAMPLES
687
	 *
688
	 * wp jetpack options list
689
	 * wp jetpack options get    <option_name>
690
	 * wp jetpack options delete <option_name>
691
	 * wp jetpack options update <option_name> [<option_value>]
692
	 *
693
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
694
	 */
695
	public function options( $args, $assoc_args ) {
696
		$action = isset( $args[0] ) ? $args[0] : 'list';
697
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
698
699
		// Is the option flagged as unsafe?
700
		$flagged = ! in_array( $args[1], $safe_to_modify );
701
702 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
703
			/* translators: %s is a command like "prompt" */
704
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
705
		}
706
707
		if ( isset( $args[0] ) ) {
708
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
709
				$action = 'get';
710
			} else if ( 'delete' == $args[0] && isset( $args[1] ) ) {
711
				$action = 'delete';
712 View Code Duplication
			} else if ( 'update' == $args[0] && isset( $args[1] ) ) {
713
				$action = 'update';
714
			} else {
715
				$action = 'list';
716
			}
717
		}
718
719
		// Bail if the option isn't found
720
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
721 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
722
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
723
		}
724
725
		// Let's print_r the option if it's an array
726
		// Used in the 'get' and 'list' actions
727
		$option = is_array( $option ) ? print_r( $option ) : $option;
728
729
		switch ( $action ) {
730
			case 'get':
731
				WP_CLI::success( "\t" . $option );
732
				break;
733
			case 'delete':
734
				jetpack_cli_are_you_sure( $flagged );
735
736
				Jetpack_Options::delete_option( $args[1] );
737
				/* translators: %s is the option name */
738
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
739
				break;
740
			case 'update':
741
				jetpack_cli_are_you_sure( $flagged );
742
743
				// Updating arrays would get pretty tricky...
744
				$value = Jetpack_Options::get_option( $args[1] );
745
				if ( $value && is_array( $value ) ) {
746
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
747
				}
748
749
				Jetpack_Options::update_option( $args[1], $args[2] );
750
				/* translators: %1$s is the previous value, %2$s is the new value */
751
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
752
				break;
753
			case 'list':
754
				$options_compact     = Jetpack_Options::get_option_names();
755
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
756
				$options_private     = Jetpack_Options::get_option_names( 'private' );
757
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
758
759
				// Table headers
760
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
761
762
				// List out the options and their values
763
				// Tell them if the value is empty or not
764
				// Tell them if it's an array
765
				foreach ( $options as $option ) {
766
					$value = Jetpack_Options::get_option( $option );
767
					if ( ! $value ) {
768
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
769
						continue;
770
					}
771
772
					if ( ! is_array( $value ) ) {
773
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
774
					} else if ( is_array( $value ) ) {
775
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
776
					}
777
				}
778
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
779
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
780
781
				WP_CLI::success(
782
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
783
					str_pad( 'wp jetpack options get', 26 )    . $option_text . "\n" .
784
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
785
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
786
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
787
				);
788
				break;
789
		}
790
	}
791
792
	/**
793
	 * Get the status of or start a new Jetpack sync.
794
	 *
795
	 * ## OPTIONS
796
	 *
797
	 * status   : Print the current sync status
798
	 * settings : Prints the current sync settings
799
	 * start    : Start a full sync from this site to WordPress.com
800
	 * enable   : Enables sync on the site
801
	 * disable  : Disable sync on a site
802
	 * reset    : Disables sync and Resets the sync queues on a site
803
	 *
804
	 * ## EXAMPLES
805
	 *
806
	 * wp jetpack sync status
807
	 * wp jetpack sync settings
808
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
809
	 * wp jetpack sync enable
810
	 * wp jetpack sync disable
811
	 * wp jetpack sync reset
812
	 * wp jetpack sync reset --queue=full or regular
813
	 *
814
	 * @synopsis <status|start> [--<field>=<value>]
815
	 */
816
	public function sync( $args, $assoc_args ) {
817
818
		$action = isset( $args[0] ) ? $args[0] : 'status';
819
820
		switch ( $action ) {
821
			case 'status':
822
				$status = Actions::get_sync_status();
823
				$collection = array();
824
				foreach ( $status as $key => $item ) {
825
					$collection[]  = array(
826
						'option' => $key,
827
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
828
					);
829
				}
830
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
831
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
832
				break;
833
			case 'settings':
834
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
835
				foreach( Jetpack_Sync_Settings::get_settings() as $setting => $item ) {
836
					$settings[]  = array(
837
						'setting' => $setting,
838
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
839
					);
840
				}
841
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
842
843
			case 'disable':
844
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
845
				update_option( 'jetpack_sync_settings_disable', 1 );
846
				/* translators: %s is the site URL */
847
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
848
				break;
849
			case 'enable':
850
				Jetpack_Sync_Settings::update_settings( array( 'disable' => 0 ) );
851
				/* translators: %s is the site URL */
852
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
853
				break;
854
			case 'reset':
855
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
856
				update_option( 'jetpack_sync_settings_disable', 1 );
857
858
				/* translators: %s is the site URL */
859
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
860
				$listener = Listener::get_instance();
861
				if ( empty( $assoc_args['queue'] ) ) {
862
					$listener->get_sync_queue()->reset();
863
					$listener->get_full_sync_queue()->reset();
864
					/* translators: %s is the site URL */
865
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
866
					break;
867
				}
868
869
				if ( ! empty( $assoc_args['queue'] ) ) {
870
					switch ( $assoc_args['queue'] ) {
871 View Code Duplication
						case 'regular':
872
							$listener->get_sync_queue()->reset();
873
							/* translators: %s is the site URL */
874
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
875
							break;
876 View Code Duplication
						case 'full':
877
							$listener->get_full_sync_queue()->reset();
878
							/* translators: %s is the site URL */
879
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
880
							break;
881
						default:
882
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
883
							break;
884
					}
885
				}
886
887
				break;
888
			case 'start':
889
				if ( ! Actions::sync_allowed() ) {
890
					if( ! Jetpack_Sync_Settings::get_setting( 'disable' ) ) {
891
						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' ) );
892
						return;
893
					}
894
					if ( doing_action( 'jetpack_user_authorized' ) || Jetpack::is_active() ) {
895
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
896
						return;
897
					}
898
					if ( Jetpack::is_development_mode() ) {
899
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in development mode.', 'jetpack' ) );
900
						return;
901
					}
902
					if (  Jetpack::is_staging_site() ) {
903
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in staging mode.', 'jetpack' ) );
904
						return;
905
					}
906
907
				}
908
				// Get the original settings so that we can restore them later
909
				$original_settings = Jetpack_Sync_Settings::get_settings();
910
911
				// Initialize sync settigns so we can sync as quickly as possible
912
				$sync_settings = wp_parse_args(
913
					array_intersect_key( $assoc_args, Jetpack_Sync_Settings::$valid_settings ),
0 ignored issues
show
The property valid_settings cannot be accessed from this context as it is declared private in class Jetpack_Sync_Settings.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
914
					array(
915
						'sync_wait_time' => 0,
916
						'enqueue_wait_time' => 0,
917
						'queue_max_writes_sec' => 10000,
918
						'max_queue_size_full_sync' => 100000
919
					)
920
				);
921
				Jetpack_Sync_Settings::update_settings( $sync_settings );
922
923
				// Convert comma-delimited string of modules to an array
924 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
925
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
926
927
					// Convert the array so that the keys are the module name and the value is true to indicate
928
					// that we want to sync the module
929
					$modules = array_map( '__return_true', array_flip( $modules ) );
930
				}
931
932 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
933
					if (
934
						'users' === $module_name &&
935
						isset( $assoc_args[ $module_name ] ) &&
936
						'initial' === $assoc_args[ $module_name ]
937
					) {
938
						$modules[ 'users' ] = 'initial';
939
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
940
						$ids = explode( ',', $assoc_args[ $module_name ] );
941
						if ( count( $ids ) > 0 ) {
942
							$modules[ $module_name ] = $ids;
943
						}
944
					}
945
				}
946
947
				if ( empty( $modules ) ) {
948
					$modules = null;
949
				}
950
951
				// Kick off a full sync
952
				if ( Actions::do_full_sync( $modules ) ) {
953
					if ( $modules ) {
954
						/* translators: %s is a comma separated list of Jetpack modules */
955
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
956
					} else {
957
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
958
					}
959 View Code Duplication
				} else {
960
961
					// Reset sync settings to original.
962
					Jetpack_Sync_Settings::update_settings( $original_settings );
963
964
					if ( $modules ) {
965
						/* translators: %s is a comma separated list of Jetpack modules */
966
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
967
					} else {
968
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
969
					}
970
				}
971
972
				// Keep sending to WPCOM until there's nothing to send
973
				$i = 1;
974
				do {
975
					$result = Actions::$sender->do_full_sync();
976
					if ( is_wp_error( $result ) ) {
977
						$queue_empty_error = ( 'empty_queue_full_sync' == $result->get_error_code() );
978
						if ( ! $queue_empty_error || ( $queue_empty_error && ( 1 == $i ) ) ) {
979
							/* translators: %s is an error code  */
980
							WP_CLI::error( sprintf( __( 'Sync errored with code: %s', 'jetpack' ), $result->get_error_code() ) );
981
						}
982
					} else {
983
						if ( 1 == $i ) {
984
							WP_CLI::log( __( 'Sent data to WordPress.com', 'jetpack' ) );
985
						} else {
986
							WP_CLI::log( __( 'Sent more data to WordPress.com', 'jetpack' ) );
987
						}
988
					}
989
					$i++;
990
				} while ( $result && ! is_wp_error( $result ) );
991
992
				// Reset sync settings to original.
993
				Jetpack_Sync_Settings::update_settings( $original_settings );
994
995
				WP_CLI::success( __( 'Finished syncing to WordPress.com', 'jetpack' ) );
996
				break;
997
		}
998
	}
999
1000
	/**
1001
	 * List the contents of a specific Jetpack sync queue.
1002
	 *
1003
	 * ## OPTIONS
1004
	 *
1005
	 * peek : List the 100 front-most items on the queue.
1006
	 *
1007
	 * ## EXAMPLES
1008
	 *
1009
	 * wp jetpack sync_queue full_sync peek
1010
	 *
1011
	 * @synopsis <incremental|full_sync> <peek>
1012
	 */
1013
	public function sync_queue( $args, $assoc_args ) {
1014
		if ( ! Actions::sync_allowed() ) {
1015
			WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site.', 'jetpack' ) );
1016
		}
1017
1018
		$queue_name = isset( $args[0] ) ? $args[0] : 'sync';
1019
		$action = isset( $args[1] ) ? $args[1] : 'peek';
1020
1021
		// We map the queue name that way we can support more friendly queue names in the commands, but still use
1022
		// the queue name that the code expects.
1023
		$queue_name_map = $allowed_queues = array(
1024
			'incremental' => 'sync',
1025
			'full'        => 'full_sync',
1026
		);
1027
		$mapped_queue_name = isset( $queue_name_map[ $queue_name ] ) ? $queue_name_map[ $queue_name ] : $queue_name;
1028
1029
		switch( $action ) {
1030
			case 'peek':
1031
				$queue = new Queue( $mapped_queue_name );
1032
				$items = $queue->peek( 100 );
1033
1034
				if ( empty( $items ) ) {
1035
					/* translators: %s is the name of the queue, either 'incremental' or 'full' */
1036
					WP_CLI::log( sprintf( __( 'Nothing is in the queue: %s', 'jetpack' ), $queue_name  ) );
1037
				} else {
1038
					$collection = array();
1039
					foreach ( $items as $item ) {
1040
						$collection[] = array(
1041
							'action'          => $item[0],
1042
							'args'            => json_encode( $item[1] ),
1043
							'current_user_id' => $item[2],
1044
							'microtime'       => $item[3],
1045
							'importing'       => (string) $item[4],
1046
						);
1047
					}
1048
					WP_CLI\Utils\format_items(
1049
						'table',
1050
						$collection,
1051
						array(
1052
							'action',
1053
							'args',
1054
							'current_user_id',
1055
							'microtime',
1056
							'importing',
1057
						)
1058
					);
1059
				}
1060
				break;
1061
		}
1062
	}
1063
1064
	/**
1065
	 * Cancel's the current Jetpack plan granted by this partner, if applicable
1066
	 *
1067
	 * Returns success or error JSON
1068
	 *
1069
	 * <token_json>
1070
	 * : JSON blob of WPCOM API token
1071
	 *  [--partner_tracking_id=<partner_tracking_id>]
1072
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1073
	 *
1074
	 *  * @synopsis <token_json> [--partner_tracking_id=<partner_tracking_id>]
1075
	 */
1076
	public function partner_cancel( $args, $named_args ) {
1077
		list( $token_json ) = $args;
1078
1079 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1080
			/* translators: %s is the invalid JSON string */
1081
			$this->partner_provision_error( new WP_Error( 'missing_access_token',  sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
1082
		}
1083
1084
		if ( isset( $token->error ) ) {
1085
			$this->partner_provision_error( new WP_Error( $token->error, $token->message ) );
1086
		}
1087
1088
		if ( ! isset( $token->access_token ) ) {
1089
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
1090
		}
1091
1092
		if ( Jetpack::validate_sync_error_idc_option() ) {
1093
			$this->partner_provision_error( new WP_Error(
1094
				'site_in_safe_mode',
1095
				esc_html__( 'Can not cancel a plan while in safe mode. See: https://jetpack.com/support/safe-mode/', 'jetpack' )
1096
			) );
1097
		}
1098
1099
		$site_identifier = Jetpack_Options::get_option( 'id' );
1100
1101
		if ( ! $site_identifier ) {
1102
			$site_identifier = Jetpack::build_raw_urls( get_home_url() );
1103
		}
1104
1105
		$request = array(
1106
			'headers' => array(
1107
				'Authorization' => "Bearer " . $token->access_token,
1108
				'Host'          => 'public-api.wordpress.com',
1109
			),
1110
			'timeout' => 60,
1111
			'method'  => 'POST',
1112
		);
1113
1114
		$url = sprintf( 'https://%s/rest/v1.3/jpphp/%s/partner-cancel', $this->get_api_host(), $site_identifier );
1115 View Code Duplication
		if ( ! empty( $named_args ) && ! empty( $named_args['partner_tracking_id'] ) ) {
1116
			$url = esc_url_raw( add_query_arg( 'partner_tracking_id', $named_args['partner_tracking_id'], $url ) );
1117
		}
1118
1119
		$result = Client::_wp_remote_request( $url, $request );
1120
1121
		Jetpack_Options::delete_option( 'onboarding' );
1122
1123
		if ( is_wp_error( $result ) ) {
1124
			$this->partner_provision_error( $result );
1125
		}
1126
1127
		WP_CLI::log( wp_remote_retrieve_body( $result ) );
1128
	}
1129
1130
	/**
1131
	 * Provision a site using a Jetpack Partner license
1132
	 *
1133
	 * Returns JSON blob
1134
	 *
1135
	 * ## OPTIONS
1136
	 *
1137
	 * <token_json>
1138
	 * : JSON blob of WPCOM API token
1139
	 * [--plan=<plan_name>]
1140
	 * : Slug of the requested plan, e.g. premium
1141
	 * [--wpcom_user_id=<user_id>]
1142
	 * : WordPress.com ID of user to connect as (must be whitelisted against partner key)
1143
	 * [--wpcom_user_email=<wpcom_user_email>]
1144
	 * : Override the email we send to WordPress.com for registration
1145
	 * [--onboarding=<onboarding>]
1146
	 * : Guide the user through an onboarding wizard
1147
	 * [--force_register=<register>]
1148
	 * : Whether to force a site to register
1149
	 * [--force_connect=<force_connect>]
1150
	 * : Force JPS to not reuse existing credentials
1151
	 * [--home_url=<home_url>]
1152
	 * : Overrides the home option via the home_url filter, or the WP_HOME constant
1153
	 * [--site_url=<site_url>]
1154
	 * : Overrides the siteurl option via the site_url filter, or the WP_SITEURL constant
1155
	 * [--partner_tracking_id=<partner_tracking_id>]
1156
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1157
	 *
1158
	 * ## EXAMPLES
1159
	 *
1160
	 *     $ wp jetpack partner_provision '{ some: "json" }' premium 1
1161
	 *     { success: true }
1162
	 *
1163
	 * @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>]
1164
	 */
1165
	public function partner_provision( $args, $named_args ) {
1166
		list( $token_json ) = $args;
1167
1168 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1169
			/* translators: %s is the invalid JSON string */
1170
			$this->partner_provision_error( new WP_Error( 'missing_access_token',  sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
1171
		}
1172
1173
		if ( isset( $token->error ) ) {
1174
			$message = isset( $token->message )
1175
				? $token->message
1176
				: '';
1177
			$this->partner_provision_error( new WP_Error( $token->error, $message ) );
1178
		}
1179
1180
		if ( ! isset( $token->access_token ) ) {
1181
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
1182
		}
1183
1184
		require_once JETPACK__PLUGIN_DIR . '_inc/class.jetpack-provision.php';
1185
1186
		$body_json = Jetpack_Provision::partner_provision( $token->access_token, $named_args );
1187
1188
		if ( is_wp_error( $body_json ) ) {
1189
			error_log( json_encode( array(
1190
				'success'       => false,
1191
				'error_code'    => $body_json->get_error_code(),
1192
				'error_message' => $body_json->get_error_message()
1193
			) ) );
1194
			exit( 1 );
1195
		}
1196
1197
		WP_CLI::log( json_encode( $body_json ) );
1198
	}
1199
1200
	/**
1201
	 * Manages your Jetpack sitemap
1202
	 *
1203
	 * ## OPTIONS
1204
	 *
1205
	 * rebuild : Rebuild all sitemaps
1206
	 * --purge : if set, will remove all existing sitemap data before rebuilding
1207
	 *
1208
	 * ## EXAMPLES
1209
	 *
1210
	 * wp jetpack sitemap rebuild
1211
	 *
1212
	 * @subcommand sitemap
1213
	 * @synopsis <rebuild> [--purge]
1214
	 */
1215
	public function sitemap( $args, $assoc_args ) {
1216
		if ( ! Jetpack::is_active() ) {
1217
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1218
		}
1219
		if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1220
			WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1221
		}
1222
		if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1223
			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' ) );
1224
		}
1225
1226
		if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1227
			$librarian = new Jetpack_Sitemap_Librarian();
1228
			$librarian->delete_all_stored_sitemap_data();
1229
		}
1230
1231
		$sitemap_builder = new Jetpack_Sitemap_Builder();
1232
		$sitemap_builder->update_sitemap();
1233
	}
1234
1235
	/**
1236
	 * Allows authorizing a user via the command line and will activate
1237
	 *
1238
	 * ## EXAMPLES
1239
	 *
1240
	 * wp jetpack authorize_user --token=123456789abcdef
1241
	 *
1242
	 * @synopsis --token=<value>
1243
	 */
1244
	public function authorize_user( $args, $named_args ) {
1245
		if ( ! is_user_logged_in() ) {
1246
			WP_CLI::error( __( 'Please select a user to authorize via the --user global argument.', 'jetpack' ) );
1247
		}
1248
1249
		if ( empty( $named_args['token'] ) ) {
1250
			WP_CLI::error( __( 'A non-empty token argument must be passed.', 'jetpack' ) );
1251
		}
1252
1253
		$is_master_user  = ! Jetpack::is_active();
1254
		$current_user_id = get_current_user_id();
1255
1256
		Jetpack::update_user_token( $current_user_id, sprintf( '%s.%d', $named_args['token'], $current_user_id ), $is_master_user );
1257
1258
		WP_CLI::log( wp_json_encode( $named_args ) );
1259
1260
		if ( $is_master_user ) {
1261
			/**
1262
			 * Auto-enable SSO module for new Jetpack Start connections
1263
			*
1264
			* @since 5.0.0
1265
			*
1266
			* @param bool $enable_sso Whether to enable the SSO module. Default to true.
1267
			*/
1268
			$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
1269
			Jetpack::handle_post_authorization_actions( $enable_sso, false );
1270
1271
			/* translators: %d is a user ID */
1272
			WP_CLI::success( sprintf( __( 'Authorized %d and activated default modules.', 'jetpack' ), $current_user_id ) );
1273
		} else {
1274
			/* translators: %d is a user ID */
1275
			WP_CLI::success( sprintf( __( 'Authorized %d.', 'jetpack' ), $current_user_id ) );
1276
		}
1277
	}
1278
1279
	/**
1280
	 * Allows calling a WordPress.com API endpoint using the current blog's token.
1281
	 *
1282
	 * ## OPTIONS
1283
	 * --resource=<resource>
1284
	 * : The resource to call with the current blog's token, where `%d` represents the current blog's ID.
1285
	 *
1286
	 * [--api_version=<api_version>]
1287
	 * : The API version to query against.
1288
	 *
1289
	 * [--base_api_path=<base_api_path>]
1290
	 * : The base API path to query.
1291
	 * ---
1292
	 * default: rest
1293
	 * ---
1294
	 *
1295
	 * [--body=<body>]
1296
	 * : A JSON encoded string representing arguments to send in the body.
1297
	 *
1298
	 * [--field=<value>]
1299
	 * : Any number of arguments that should be passed to the resource.
1300
	 *
1301
	 * [--pretty]
1302
	 * : Will pretty print the results of a successful API call.
1303
	 *
1304
	 * [--strip-success]
1305
	 * : Will remove the green success label from successful API calls.
1306
	 *
1307
	 * ## EXAMPLES
1308
	 *
1309
	 * wp jetpack call_api --resource='/sites/%d'
1310
	 */
1311
	public function call_api( $args, $named_args ) {
1312
		if ( ! Jetpack::is_active() ) {
1313
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1314
		}
1315
1316
		$consumed_args = array(
1317
			'resource',
1318
			'api_version',
1319
			'base_api_path',
1320
			'body',
1321
			'pretty',
1322
		);
1323
1324
		// Get args that should be passed to resource.
1325
		$other_args = array_diff_key( $named_args, array_flip( $consumed_args ) );
1326
1327
		$decoded_body = ! empty( $named_args['body'] )
1328
			? json_decode( $named_args['body'], true )
1329
			: false;
1330
1331
		$resource_url = ( false === strpos( $named_args['resource'], '%d' ) )
1332
			? $named_args['resource']
1333
			: sprintf( $named_args['resource'], Jetpack_Options::get_option( 'id' ) );
1334
1335
		$response = Client::wpcom_json_api_request_as_blog(
1336
			$resource_url,
1337
			empty( $named_args['api_version'] ) ? Client::WPCOM_JSON_API_VERSION : $named_args['api_version'],
1338
			$other_args,
1339
			empty( $decoded_body ) ? null : $decoded_body,
1340
			empty( $named_args['base_api_path'] ) ? 'rest' : $named_args['base_api_path']
1341
		);
1342
1343 View Code Duplication
		if ( is_wp_error( $response ) ) {
1344
			WP_CLI::error( sprintf(
1345
				/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an error code, %3$s is an error message. */
1346
				__( 'Request to %1$s returned an error: (%2$d) %3$s.', 'jetpack' ),
1347
				$resource_url,
1348
				$response->get_error_code(),
1349
				$response->get_error_message()
1350
			) );
1351
		}
1352
1353
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1354
			WP_CLI::error( sprintf(
1355
				/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an HTTP status code. */
1356
				__( 'Request to %1$s returned a non-200 response code: %2$d.', 'jetpack' ),
1357
				$resource_url,
1358
				wp_remote_retrieve_response_code( $response )
1359
			) );
1360
		}
1361
1362
		$output = wp_remote_retrieve_body( $response );
1363
		if ( isset( $named_args['pretty'] ) ) {
1364
			$decoded_output = json_decode( $output );
1365
			if ( $decoded_output ) {
1366
				$output = wp_json_encode( $decoded_output, JSON_PRETTY_PRINT );
1367
			}
1368
		}
1369
1370
		if ( isset( $named_args['strip-success'] ) ) {
1371
			WP_CLI::log( $output );
1372
			WP_CLI::halt( 0 );
1373
		}
1374
1375
		WP_CLI::success( $output );
1376
	}
1377
1378
	/**
1379
	 * Allows uploading SSH Credentials to the current site for backups, restores, and security scanning.
1380
	 *
1381
	 * ## OPTIONS
1382
	 *
1383
	 * [--host=<host>]
1384
	 * : The SSH server's address.
1385
	 *
1386
	 * [--ssh-user=<user>]
1387
	 * : The username to use to log in to the SSH server.
1388
	 *
1389
	 * [--pass=<pass>]
1390
	 * : The password used to log in, if using a password. (optional)
1391
	 *
1392
	 * [--kpri=<kpri>]
1393
	 * : The private key used to log in, if using a private key. (optional)
1394
	 *
1395
	 * [--pretty]
1396
	 * : Will pretty print the results of a successful API call. (optional)
1397
	 *
1398
	 * [--strip-success]
1399
	 * : Will remove the green success label from successful API calls. (optional)
1400
	 *
1401
	 * ## EXAMPLES
1402
	 *
1403
	 * wp jetpack upload_ssh_creds --host=example.com --ssh-user=example --pass=password
1404
	 * wp jetpack updload_ssh_creds --host=example.com --ssh-user=example --kpri=key
1405
	 */
1406
	public function upload_ssh_creds( $args, $named_args ) {
1407
		if ( ! Jetpack::is_active() ) {
1408
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1409
		}
1410
1411
		$required_args = array(
1412
			'host',
1413
			'ssh-user',
1414
		);
1415
1416
		foreach ( $required_args as $arg ) {
1417
			if ( empty( $named_args[ $arg ] ) ) {
1418
				WP_CLI::error(
1419
					sprintf(
1420
						/* translators: %s is a slug, such as 'host'. */
1421
						__( '`%s` cannot be empty.', 'jetpack' ),
1422
						$arg
1423
					)
1424
				);
1425
			}
1426
		}
1427
1428
		if ( empty( $named_args['pass'] ) && empty( $named_args['kpri'] ) ) {
1429
			WP_CLI::error( __( 'Both `pass` and `kpri` fields cannot be blank.', 'jetpack' ) );
1430
		}
1431
1432
		$values = array(
1433
			'credentials' => array(
1434
				'site_url' => get_site_url(),
1435
				'abspath'  => ABSPATH,
1436
				'protocol' => 'ssh',
1437
				'port'     => 22,
1438
				'role'     => 'main',
1439
				'host'     => $named_args['host'],
1440
				'user'     => $named_args['ssh-user'],
1441
				'pass'     => empty( $named_args['pass'] ) ? '' : $named_args['pass'],
1442
				'kpri'     => empty( $named_args['kpri'] ) ? '' : $named_args['kpri'],
1443
			),
1444
		);
1445
1446
		$named_args = wp_parse_args(
1447
			array(
1448
				'resource'    => '/activity-log/%d/update-credentials',
1449
				'method'      => 'POST',
1450
				'api_version' => '1.1',
1451
				'body'        => wp_json_encode( $values ),
1452
				'timeout'     => 30,
1453
			),
1454
			$named_args
1455
		);
1456
1457
		self::call_api( $args, $named_args );
1458
	}
1459
1460
	/**
1461
	 * API wrapper for getting stats from the WordPress.com API for the current site.
1462
	 *
1463
	 * ## OPTIONS
1464
	 *
1465
	 * [--quantity=<quantity>]
1466
	 * : The number of units to include.
1467
	 * ---
1468
	 * default: 30
1469
	 * ---
1470
	 *
1471
	 * [--period=<period>]
1472
	 * : The unit of time to query stats for.
1473
	 * ---
1474
	 * default: day
1475
	 * options:
1476
	 *  - day
1477
	 *  - week
1478
	 *  - month
1479
	 *  - year
1480
	 * ---
1481
	 *
1482
	 * [--date=<date>]
1483
	 * : The latest date to return stats for. Ex. - 2018-01-01.
1484
	 *
1485
	 * [--pretty]
1486
	 * : Will pretty print the results of a successful API call.
1487
	 *
1488
	 * [--strip-success]
1489
	 * : Will remove the green success label from successful API calls.
1490
	 *
1491
	 * ## EXAMPLES
1492
	 *
1493
	 * wp jetpack get_stats
1494
	 */
1495
	public function get_stats( $args, $named_args ) {
1496
		$selected_args = array_intersect_key(
1497
			$named_args,
1498
			array_flip( array(
1499
				'quantity',
1500
				'date',
1501
			) )
1502
		);
1503
1504
		// The API expects unit, but period seems to be more correct.
1505
		$selected_args['unit'] = $named_args['period'];
1506
1507
		$command = sprintf(
1508
			'jetpack call_api --resource=/sites/%d/stats/%s',
1509
			Jetpack_Options::get_option( 'id' ),
1510
			add_query_arg( $selected_args, 'visits' )
1511
		);
1512
1513
		if ( isset( $named_args['pretty'] ) ) {
1514
			$command .= ' --pretty';
1515
		}
1516
1517
		if ( isset( $named_args['strip-success'] ) ) {
1518
			$command .= ' --strip-success';
1519
		}
1520
1521
		WP_CLI::runcommand(
1522
			$command,
1523
			array(
1524
				'launch' => false, // Use the current process.
1525
			)
1526
		);
1527
	}
1528
1529
	/**
1530
	 * Allows management of publicize connections.
1531
	 *
1532
	 * ## OPTIONS
1533
	 *
1534
	 * <list|disconnect>
1535
	 * : The action to perform.
1536
	 * ---
1537
	 * options:
1538
	 *   - list
1539
	 *   - disconnect
1540
	 * ---
1541
	 *
1542
	 * [<identifier>]
1543
	 * : The connection ID or service to perform an action on.
1544
	 *
1545
	 * [--format=<format>]
1546
	 * : Allows overriding the output of the command when listing connections.
1547
	 * ---
1548
	 * default: table
1549
	 * options:
1550
	 *   - table
1551
	 *   - json
1552
	 *   - csv
1553
	 *   - yaml
1554
	 *   - ids
1555
	 *   - count
1556
	 * ---
1557
	 *
1558
	 * ## EXAMPLES
1559
	 *
1560
	 *     # List all publicize connections.
1561
	 *     $ wp jetpack publicize list
1562
	 *
1563
	 *     # List publicize connections for a given service.
1564
	 *     $ wp jetpack publicize list twitter
1565
	 *
1566
	 *     # List all publicize connections for a given user.
1567
	 *     $ wp --user=1 jetpack publicize list
1568
	 *
1569
	 *     # List all publicize connections for a given user and service.
1570
	 *     $ wp --user=1 jetpack publicize list twitter
1571
	 *
1572
	 *     # Display details for a given connection.
1573
	 *     $ wp jetpack publicize list 123456
1574
	 *
1575
	 *     # Diconnection a given connection.
1576
	 *     $ wp jetpack publicize disconnect 123456
1577
	 *
1578
	 *     # Disconnect all connections.
1579
	 *     $ wp jetpack publicize disconnect all
1580
	 *
1581
	 *     # Disconnect all connections for a given service.
1582
	 *     $ wp jetpack publicize disconnect twitter
1583
	 */
1584
	public function publicize( $args, $named_args ) {
1585
		if ( ! Jetpack::is_active() ) {
1586
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1587
		}
1588
1589
		if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1590
			WP_CLI::error( __( 'The publicize module is not active.', 'jetpack' ) );
1591
		}
1592
1593
		if ( Jetpack::is_development_mode() ) {
1594
			if (
1595
				! defined( 'JETPACK_DEV_DEBUG' ) &&
1596
				! has_filter( 'jetpack_development_mode' ) &&
1597
				false === strpos( site_url(), '.' )
1598
			) {
1599
				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' ) );
1600
			}
1601
1602
			WP_CLI::error( __( 'Jetpack is currently in development mode, so the publicize module will not load.', 'jetpack' ) );
1603
		}
1604
1605
		if ( ! class_exists( 'Publicize' ) ) {
1606
			WP_CLI::error( __( 'The publicize module is not loaded.', 'jetpack' ) );
1607
		}
1608
1609
		$action        = $args[0];
1610
		$publicize     = new Publicize();
1611
		$identifier    = ! empty( $args[1] ) ? $args[1] : false;
1612
		$services      = array_keys( $publicize->get_services() );
1613
		$id_is_service = in_array( $identifier, $services, true );
1614
1615
		switch ( $action ) {
1616
			case 'list':
1617
				$connections_to_return = array();
1618
1619
				// For the CLI command, let's return all connections when a user isn't specified. This
1620
				// differs from the logic in the Publicize class.
1621
				$option_connections = is_user_logged_in()
1622
					? (array) $publicize->get_all_connections_for_user()
1623
					: (array) $publicize->get_all_connections();
1624
1625
				foreach ( $option_connections as $service_name => $connections ) {
1626
					foreach ( (array) $connections as $id => $connection ) {
1627
						$connection['id']        = $id;
1628
						$connection['service']   = $service_name;
1629
						$connections_to_return[] = $connection;
1630
					}
1631
				}
1632
1633
				if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1634
					$temp_connections      = $connections_to_return;
1635
					$connections_to_return = array();
1636
1637
					foreach ( $temp_connections as $connection ) {
1638
						if ( $identifier === $connection['service'] ) {
1639
							$connections_to_return[] = $connection;
1640
						}
1641
					}
1642
				}
1643
1644
				if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1645
					$connections_to_return = wp_list_filter( $connections_to_return, array( 'id' => $identifier ) );
1646
				}
1647
1648
				$expected_keys = array(
1649
					'id',
1650
					'service',
1651
					'user_id',
1652
					'provider',
1653
					'issued',
1654
					'expires',
1655
					'external_id',
1656
					'external_name',
1657
					'external_display',
1658
					'type',
1659
					'connection_data',
1660
				);
1661
1662
				// Somehow, a test site ended up in a state where $connections_to_return looked like:
1663
				// array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1664
				// This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1665
				// to minimize future issues, this nested loop will remove any connections that don't contain
1666
				// any keys that we expect.
1667
				foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1668
					foreach ( $expected_keys as $expected_key ) {
1669
						if ( ! isset( $connection[ $expected_key ] ) ) {
1670
							unset( $connections_to_return[ $connection_key ] );
1671
							continue;
1672
						}
1673
					}
1674
				}
1675
1676
				if ( empty( $connections_to_return ) ) {
1677
					return false;
1678
				}
1679
1680
				WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1681
				break; // list.
1682
			case 'disconnect':
1683
				if ( ! $identifier ) {
1684
					WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1685
				}
1686
1687
				// If the connection ID is 'all' then delete all connections. If the connection ID
1688
				// matches a service, delete all connections for that service.
1689
				if ( 'all' === $identifier || $id_is_service ) {
1690
					if ( 'all' === $identifier ) {
1691
						WP_CLI::log( __( "You're about to delete all publicize connections.", 'jetpack' ) );
1692
					} else {
1693
						/* translators: %s is a lowercase string for a social network. */
1694
						WP_CLI::log( sprintf( __( "You're about to delete all publicize connections to %s.", 'jetpack' ), $identifier ) );
1695
					}
1696
1697
					jetpack_cli_are_you_sure();
1698
1699
					$connections = array();
1700
					$service     = $identifier;
1701
1702
					$option_connections = is_user_logged_in()
1703
						? (array) $publicize->get_all_connections_for_user()
1704
						: (array) $publicize->get_all_connections();
1705
1706
					if ( 'all' === $service ) {
1707
						foreach ( (array) $option_connections as $service_name => $service_connections ) {
1708
							foreach ( $service_connections as $id => $connection ) {
1709
								$connections[ $id ] = $connection;
1710
							}
1711
						}
1712
					} elseif ( ! empty( $option_connections[ $service ] ) ) {
1713
						$connections = $option_connections[ $service ];
1714
					}
1715
1716
					if ( ! empty( $connections ) ) {
1717
						$count    = count( $connections );
1718
						$progress = \WP_CLI\Utils\make_progress_bar(
1719
							/* translators: %s is a lowercase string for a social network. */
1720
							sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1721
							$count
1722
						);
1723
1724
						foreach ( $connections as $id => $connection ) {
1725
							if ( false === $publicize->disconnect( false, $id ) ) {
1726
								WP_CLI::error( sprintf(
1727
									/* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1728
									__( 'Publicize connection %d could not be disconnected', 'jetpack' ),
1729
									$id
1730
								) );
1731
							}
1732
1733
							$progress->tick();
1734
						}
1735
1736
						$progress->finish();
1737
1738
						if ( 'all' === $service ) {
1739
							WP_CLI::success( __( 'All publicize connections were successfully disconnected.', 'jetpack' ) );
1740
						} else {
1741
							/* translators: %s is a lowercase string for a social network. */
1742
							WP_CLI::success( __( 'All publicize connections to %s were successfully disconnected.', 'jetpack' ), $service );
1743
						}
1744
					}
1745
				} else {
1746
					if ( false !== $publicize->disconnect( false, $identifier ) ) {
1747
						/* translators: %d is a numeric ID. Example: 1234. */
1748
						WP_CLI::success( sprintf( __( 'Publicize connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1749
					} else {
1750
						/* translators: %d is a numeric ID. Example: 1234. */
1751
						WP_CLI::error( sprintf( __( 'Publicize connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1752
					}
1753
				}
1754
				break; // disconnect.
1755
		}
1756
	}
1757
1758
	private function get_api_host() {
1759
		$env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1760
		return $env_api_host ? $env_api_host : JETPACK__WPCOM_JSON_API_HOST;
1761
	}
1762
1763
	private function partner_provision_error( $error ) {
1764
		WP_CLI::log( json_encode( array(
1765
			'success'       => false,
1766
			'error_code'    => $error->get_error_code(),
1767
			'error_message' => $error->get_error_message()
1768
		) ) );
1769
		exit( 1 );
1770
	}
1771
1772
	/**
1773
	 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1774
	 *
1775
	 * ## TYPES
1776
	 *
1777
	 * 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.
1778
	 *
1779
	 * ## BLOCK TYPE OPTIONS
1780
	 *
1781
	 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1782
	 * 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.
1783
	 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1784
	 * --description: Allows to provide a text description of the block.
1785
	 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1786
	 *
1787
	 * ## BLOCK TYPE EXAMPLES
1788
	 *
1789
	 * wp jetpack scaffold block "Cool Block"
1790
	 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1791
	 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1792
	 *
1793
	 * @subcommand scaffold block
1794
	 * @synopsis <type> <title> [--slug] [--description] [--keywords]
1795
	 *
1796
	 * @param array $args       Positional parameters, when strings are passed, wrap them in quotes.
1797
	 * @param array $assoc_args Associative parameters like --slug="nice-block".
1798
	 */
1799
	public function scaffold( $args, $assoc_args ) {
1800
		// It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1801
		switch ( $args[0] ) {
1802
			case 'block':
1803
				$this->block( $args, $assoc_args );
1804
				break;
1805
			default:
1806
				/* translators: %s is the subcommand */
1807
				WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1808
				exit( 1 );
1809
		}
1810
	}
1811
1812
	/**
1813
	 * Creates the essential files in Jetpack to build a Gutenberg block.
1814
	 *
1815
	 * @param array $args       Positional parameters. Only one is used, that corresponds to the block title.
1816
	 * @param array $assoc_args Associative parameters defined in the scaffold() method.
1817
	 */
1818
	public function block( $args, $assoc_args ) {
1819 View Code Duplication
		if ( isset( $args[1] ) ) {
1820
			$title = ucwords( $args[1] );
1821
		} else {
1822
			WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
1823
			exit( 1 );
1824
		}
1825
1826
		$slug = isset( $assoc_args['slug'] )
1827
			? $assoc_args['slug']
1828
			: sanitize_title( $title );
1829
1830
		if ( preg_match( '#^jetpack/#', $slug ) ) {
1831
			$slug = preg_replace( '#^jetpack/#', '', $slug );
1832
		}
1833
1834
		if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
1835
			WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
1836
		}
1837
1838
		global $wp_filesystem;
1839
		if ( ! WP_Filesystem() ) {
1840
			WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
1841
		}
1842
1843
		$path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
1844
1845
		if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
1846
			/* translators: %s is path to the conflicting block */
1847
			WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
1848
			exit( 1 );
1849
		}
1850
1851
		$wp_filesystem->mkdir( $path );
1852
1853
		$hasKeywords = isset( $assoc_args['keywords'] );
1854
1855
		$files = array(
1856
			"$path/$slug.php" => $this->render_block_file( 'block-register-php', array(
1857
				'slug' => $slug,
1858
				'title' => $title,
1859
				'underscoredSlug' => str_replace( '-', '_', $slug ),
1860
			) ),
1861
			"$path/index.js" => $this->render_block_file( 'block-index-js', array(
1862
				'slug' => $slug,
1863
				'title' => $title,
1864
				'description' => isset( $assoc_args['description'] )
1865
					? $assoc_args['description']
1866
					: $title,
1867
				'keywords' => $hasKeywords
1868
					? array_map( function( $keyword ) {
1869
						// Construction necessary for Mustache lists
1870
						return array( 'keyword' => trim( $keyword ) );
1871
					}, explode( ',', $assoc_args['keywords'], 3 ) )
1872
					: '',
1873
				'hasKeywords' => $hasKeywords
1874
			) ),
1875
			"$path/editor.js" => $this->render_block_file( 'block-editor-js' ),
1876
			"$path/editor.scss" => $this->render_block_file( 'block-editor-scss', array(
1877
				'slug' => $slug,
1878
				'title' => $title,
1879
			) ),
1880
			"$path/edit.js" => $this->render_block_file( 'block-edit-js', array(
1881
				'title' => $title,
1882
				'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
1883
			) )
1884
		);
1885
1886
		$files_written = array();
1887
1888
		foreach ( $files as $filename => $contents ) {
1889
			if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
1890
				$files_written[] = $filename;
1891
			} else {
1892
				/* translators: %s is a file name */
1893
				WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
1894
			}
1895
		}
1896
1897
		if ( empty( $files_written ) ) {
1898
			WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
1899
		} else {
1900
			// Load index.json and insert the slug of the new block in the production array
1901
			$block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
1902
			$block_list = $wp_filesystem->get_contents( $block_list_path );
1903
			if ( empty( $block_list ) ) {
1904
				/* translators: %s is the path to the file with the block list */
1905
				WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
1906
			} else if ( false === stripos( $block_list, $slug ) ) {
1907
				$new_block_list = json_decode( $block_list );
1908
				$new_block_list->beta[] = $slug;
1909
				if ( ! $wp_filesystem->put_contents( $block_list_path, wp_json_encode( $new_block_list ) ) ) {
1910
					/* translators: %s is the path to the file with the block list */
1911
					WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
1912
				}
1913
			}
1914
1915
			WP_CLI::success( sprintf(
1916
				/* translators: the placeholders are a human readable title, and a series of words separated by dashes */
1917
				esc_html__( 'Successfully created block %s with slug %s', 'jetpack' ) . ' 🎉' . "\n" .
1918
				"--------------------------------------------------------------------------------------------------------------------\n" .
1919
				/* translators: the placeholder is a directory path */
1920
				esc_html__( 'The files were created at %s', 'jetpack' ) . "\n" .
1921
				esc_html__( 'To start using the block, build the blocks with yarn run build-extensions', 'jetpack' ) . "\n" .
1922
				/* translators: the placeholder is a file path */
1923
				esc_html__( 'The block slug has been added to the beta list at %s', 'jetpack' ) . "\n" .
1924
				esc_html__( 'To load the block, add the constant JETPACK_BETA_BLOCKS as true to your wp-config.php file', 'jetpack' ) . "\n" .
1925
				/* translators: the placeholder is a URL */
1926
				"\n" . esc_html__( 'Read more at %s', 'jetpack' ) . "\n",
1927
				$title,
1928
				$slug,
1929
				$path,
1930
				$block_list_path,
1931
				'https://github.com/Automattic/jetpack/blob/master/extensions/README.md#develop-new-blocks'
1932
			) . '--------------------------------------------------------------------------------------------------------------------' );
1933
		}
1934
	}
1935
1936
	/**
1937
	 * Built the file replacing the placeholders in the template with the data supplied.
1938
	 *
1939
	 * @param string $template
1940
	 * @param array $data
1941
	 *
1942
	 * @return string mixed
1943
	 */
1944
	private static function render_block_file( $template, $data = array() ) {
1945
		return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
1946
	}
1947
}
1948
1949
/*
1950
 * Standard "ask for permission to continue" function.
1951
 * If action cancelled, ask if they need help.
1952
 *
1953
 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
1954
 *
1955
 * @param $flagged   bool   false = normal option | true = flagged by get_jetpack_options_for_reset()
1956
 * @param $error_msg string (optional)
1957
 */
1958
function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
1959
	$cli = new Jetpack_CLI();
1960
1961
	// Default cancellation message
1962
	if ( ! $error_msg ) {
1963
		$error_msg =
1964
			__( 'Action cancelled. Have a question?', 'jetpack' )
1965
			. ' '
1966
			. $cli->green_open
1967
			. 'jetpack.com/support'
1968
			.  $cli->color_close;
1969
	}
1970
1971
	if ( ! $flagged ) {
1972
		$prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
1973
	} else {
1974
		$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' );
1975
	}
1976
1977
	WP_CLI::line( $prompt_message );
1978
	$handle = fopen( "php://stdin", "r" );
1979
	$line = fgets( $handle );
1980
	if ( 'yes' != trim( $line ) ){
1981
		WP_CLI::error( $error_msg );
1982
	}
1983
}
1984