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