Completed
Push — try/sync-package ( 228b13 )
by Marin
07:37
created

class.jetpack-cli.php (2 issues)

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
use Automattic\Jetpack\Sync\Actions;
4
use Automattic\Jetpack\Sync\Listener;
5
use Automattic\Jetpack\Sync\Queue;
6
use Automattic\Jetpack\Sync\Settings;
7
8
WP_CLI::add_command( 'jetpack', 'Jetpack_CLI' );
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 = Jetpack_Client::wpcom_json_api_request_as_blog(
139
			sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
140
			Jetpack_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
327
				// Jumpstart option is special
328
				if ( ! $is_dry_run ) {
329
					Jetpack_Options::update_option( 'jumpstart', 'new_connection' );
330
				}
331
				WP_CLI::success( __( 'jumpstart option reset', 'jetpack' ) );
332
				break;
333 View Code Duplication
			case 'modules':
334
				if ( ! $is_dry_run ) {
335
					$default_modules = Jetpack::get_default_modules();
336
					Jetpack::update_active_modules( $default_modules );
337
				}
338
339
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
340
				break;
341
			case 'prompt':
342
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
343
				break;
344
			case 'sync-checksum':
345
				$option = 'jetpack_callables_sync_checksum';
346
347
				if ( is_multisite() ) {
348
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
349
350
					/*
351
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
352
					 * Offset can be used to paginate and try to clean up more sites.
353
					 */
354
					$sites       = get_sites( array( 'number' => 1000, 'offset' => $offset ) );
355
					$count_fixes = 0;
356
					foreach ( $sites as $site ) {
357
						switch_to_blog( $site->blog_id );
358
						$count = self::count_option( $option );
359
						if ( $count > 1 ) {
360
							if ( ! $is_dry_run ) {
361
								delete_option( $option );
362
							}
363
							WP_CLI::line(
364
								sprintf(
365
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
366
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
367
									$count,
368
									$option,
369
									"{$site->domain}{$site->path}"
370
								)
371
							);
372
							$count_fixes++;
373
							if ( ! $is_dry_run ) {
374
								/*
375
								 * We could be deleting a lot of options rows at the same time.
376
								 * Allow some time for replication to catch up.
377
								 */
378
								sleep( 3 );
379
							}
380
						}
381
382
						restore_current_blog();
383
					}
384
					if ( $count_fixes ) {
385
						WP_CLI::success(
386
							sprintf(
387
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
388
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
389
								$option,
390
								$count_fixes
391
							)
392
						);
393
					} else {
394
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
395
					}
396
					return;
397
				}
398
399
				$count = self::count_option( $option );
400
				if ( $count > 1 ) {
401
					if ( ! $is_dry_run ) {
402
						delete_option( $option );
403
					}
404
					WP_CLI::success(
405
						sprintf(
406
							/* translators: %1$d is a number, %2$s is the name of an option. */
407
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
408
							$count,
409
							$option
410
						)
411
					);
412
					return;
413
				}
414
415
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
416
				break;
417
418
		}
419
	}
420
421
	/**
422
	 * Return the number of times an option appears
423
	 * Normally an option would only appear 1 since the option key is supposed to be unique
424
	 * but if a site hasn't updated the DB schema then that would not be the case.
425
	 *
426
	 * @param string $option Option name.
427
	 *
428
	 * @return int
429
	 */
430
	private static function count_option( $option ) {
431
		global $wpdb;
432
		return (int) $wpdb->get_var(
433
			$wpdb->prepare(
434
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
435
				$option
436
			)
437
		);
438
439
	}
440
441
	/**
442
	 * Manage Jetpack Modules
443
	 *
444
	 * ## OPTIONS
445
	 *
446
	 * <list|activate|deactivate|toggle>
447
	 * : The action to take.
448
	 * ---
449
	 * default: list
450
	 * options:
451
	 *  - list
452
	 *  - activate
453
	 *  - deactivate
454
	 *  - toggle
455
	 * ---
456
	 *
457
	 * [<module_slug>]
458
	 * : The slug of the module to perform an action on.
459
	 *
460
	 * [--format=<format>]
461
	 * : Allows overriding the output of the command when listing modules.
462
	 * ---
463
	 * default: table
464
	 * options:
465
	 *  - table
466
	 *  - json
467
	 *  - csv
468
	 *  - yaml
469
	 *  - ids
470
	 *  - count
471
	 * ---
472
	 *
473
	 * ## EXAMPLES
474
	 *
475
	 * wp jetpack module list
476
	 * wp jetpack module list --format=json
477
	 * wp jetpack module activate stats
478
	 * wp jetpack module deactivate stats
479
	 * wp jetpack module toggle stats
480
	 * wp jetpack module activate all
481
	 * wp jetpack module deactivate all
482
	 */
483
	public function module( $args, $assoc_args ) {
484
		$action = isset( $args[0] ) ? $args[0] : 'list';
485
486
		if ( isset( $args[1] ) ) {
487
			$module_slug = $args[1];
488
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
489
				/* translators: %s is a module slug like "stats" */
490
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
491
			}
492
			if ( 'toggle' === $action ) {
493
				$action = Jetpack::is_module_active( $module_slug )
494
					? 'deactivate'
495
					: 'activate';
496
			}
497
			if ( 'all' === $args[1] ) {
498
				$action = ( 'deactivate' === $action )
499
					? 'deactivate_all'
500
					: 'activate_all';
501
			}
502
		} elseif ( 'list' !== $action ) {
503
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
504
			$action = 'list';
505
		}
506
507
		switch ( $action ) {
508
			case 'list':
509
				$modules_list = array();
510
				$modules      = Jetpack::get_available_modules();
511
				sort( $modules );
512
				foreach ( (array) $modules as $module_slug ) {
513
					if ( 'vaultpress' === $module_slug ) {
514
						continue;
515
					}
516
					$modules_list[] = array(
517
						'slug'   => $module_slug,
518
						'status' => Jetpack::is_module_active( $module_slug )
519
							? __( 'Active', 'jetpack' )
520
							: __( 'Inactive', 'jetpack' ),
521
					);
522
				}
523
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
524
				break;
525
			case 'activate':
526
				$module = Jetpack::get_module( $module_slug );
527
				Jetpack::log( 'activate', $module_slug );
528
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
529
					/* translators: %s is the name of a Jetpack module */
530
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
531
				} else {
532
					/* translators: %s is the name of a Jetpack module */
533
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
534
				}
535
				break;
536 View Code Duplication
			case 'activate_all':
537
				$modules = Jetpack::get_available_modules();
538
				Jetpack::update_active_modules( $modules );
539
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
540
				break;
541
			case 'deactivate':
542
				$module = Jetpack::get_module( $module_slug );
543
				Jetpack::log( 'deactivate', $module_slug );
544
				Jetpack::deactivate_module( $module_slug );
545
				/* translators: %s is the name of a Jetpack module */
546
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
547
				break;
548
			case 'deactivate_all':
549
				Jetpack::delete_active_modules();
550
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
551
				break;
552
			case 'toggle':
553
				// Will never happen, should have been handled above and changed to activate or deactivate.
554
				break;
555
		}
556
	}
557
558
	/**
559
	 * Manage Protect Settings
560
	 *
561
	 * ## OPTIONS
562
	 *
563
	 * whitelist: Whitelist an IP address.  You can also read or clear the whitelist.
564
	 *
565
	 *
566
	 * ## EXAMPLES
567
	 *
568
	 * wp jetpack protect whitelist <ip address>
569
	 * wp jetpack protect whitelist list
570
	 * wp jetpack protect whitelist clear
571
	 *
572
	 * @synopsis <whitelist> [<ip|ip_low-ip_high|list|clear>]
573
	 */
574
	public function protect( $args, $assoc_args ) {
575
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
576
		if ( ! in_array( $action, array( 'whitelist' ) ) ) {
577
			/* translators: %s is a command like "prompt" */
578
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
579
		}
580
		// Check if module is active
581
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
582
			/* translators: %s is a module name */
583
			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__ ) );
584
		}
585
		if ( in_array( $action, array( 'whitelist' ) ) ) {
586
			if ( isset( $args[1] ) ) {
587
				$action = 'whitelist';
588
			} else {
589
				$action = 'prompt';
590
			}
591
		}
592
		switch ( $action ) {
593
			case 'whitelist':
594
				$whitelist         = array();
595
				$new_ip            = $args[1];
596
				$current_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
597
598
				// Build array of IPs that are already whitelisted.
599
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
600
				// low & high range params for jetpack_protect_ip_address_is_in_range();
601
				foreach( $current_whitelist as $whitelisted ) {
602
603
					// IP ranges
604
					if ( $whitelisted->range ) {
605
606
						// Is it already whitelisted?
607
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $whitelisted->range_low, $whitelisted->range_high ) ) {
608
							/* translators: %s is an IP address */
609
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
610
							break;
611
						}
612
						$whitelist[] = $whitelisted->range_low . " - " . $whitelisted->range_high;
613
614
					} else { // Individual IPs
615
616
						// Check if the IP is already whitelisted (single IP only)
617
						if ( $new_ip == $whitelisted->ip_address ) {
618
							/* translators: %s is an IP address */
619
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
620
							break;
621
						}
622
						$whitelist[] = $whitelisted->ip_address;
623
624
					}
625
				}
626
627
				/*
628
				 * List the whitelist
629
				 * Done here because it's easier to read the $whitelist array after it's been rebuilt
630
				 */
631
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
632
					if ( ! empty( $whitelist ) ) {
633
						WP_CLI::success( __( 'Here are your whitelisted IPs:', 'jetpack' ) );
634
						foreach ( $whitelist as $ip ) {
635
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) ) ;
636
						}
637
					} else {
638
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
639
					}
640
					break;
641
				}
642
643
				/*
644
				 * Clear the whitelist
645
				 */
646
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
647
					if ( ! empty( $whitelist ) ) {
648
						$whitelist = array();
649
						jetpack_protect_save_whitelist( $whitelist );
650
						WP_CLI::success( __( 'Cleared all whitelisted IPs', 'jetpack' ) );
651
					} else {
652
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
653
					}
654
					break;
655
				}
656
657
				// Append new IP to whitelist array
658
				array_push( $whitelist, $new_ip );
659
660
				// Save whitelist if there are no errors
661
				$result = jetpack_protect_save_whitelist( $whitelist );
662
				if ( is_wp_error( $result ) ) {
663
					WP_CLI::error( $result );
664
				}
665
666
				/* translators: %s is an IP address */
667
				WP_CLI::success( sprintf( __( '%s has been whitelisted.', 'jetpack' ), $new_ip ) );
668
				break;
669
			case 'prompt':
670
				WP_CLI::error(
671
					__( 'No command found.', 'jetpack' ) . "\n" .
672
					__( 'Please enter the IP address you want to whitelist.', 'jetpack' ) . "\n" .
673
					_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" .
674
					_x( "You can also 'list' or 'clear' the whitelist.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
675
				);
676
				break;
677
		}
678
	}
679
680
	/**
681
	 * Manage Jetpack Options
682
	 *
683
	 * ## OPTIONS
684
	 *
685
	 * list   : List all jetpack options and their values
686
	 * delete : Delete an option
687
	 *          - can only delete options that are white listed.
688
	 * update : update an option
689
	 *          - can only update option strings
690
	 * get    : get the value of an option
691
	 *
692
	 * ## EXAMPLES
693
	 *
694
	 * wp jetpack options list
695
	 * wp jetpack options get    <option_name>
696
	 * wp jetpack options delete <option_name>
697
	 * wp jetpack options update <option_name> [<option_value>]
698
	 *
699
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
700
	 */
701
	public function options( $args, $assoc_args ) {
702
		$action = isset( $args[0] ) ? $args[0] : 'list';
703
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
704
705
		// Jumpstart is special
706
		array_push( $safe_to_modify, 'jumpstart' );
707
708
		// Is the option flagged as unsafe?
709
		$flagged = ! in_array( $args[1], $safe_to_modify );
710
711 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
712
			/* translators: %s is a command like "prompt" */
713
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
714
		}
715
716
		if ( isset( $args[0] ) ) {
717
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
718
				$action = 'get';
719
			} else if ( 'delete' == $args[0] && isset( $args[1] ) ) {
720
				$action = 'delete';
721 View Code Duplication
			} else if ( 'update' == $args[0] && isset( $args[1] ) ) {
722
				$action = 'update';
723
			} else {
724
				$action = 'list';
725
			}
726
		}
727
728
		// Bail if the option isn't found
729
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
730 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
731
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
732
		}
733
734
		// Let's print_r the option if it's an array
735
		// Used in the 'get' and 'list' actions
736
		$option = is_array( $option ) ? print_r( $option ) : $option;
737
738
		switch ( $action ) {
739
			case 'get':
740
				WP_CLI::success( "\t" . $option );
741
				break;
742
			case 'delete':
743
				jetpack_cli_are_you_sure( $flagged );
744
745
				Jetpack_Options::delete_option( $args[1] );
746
				/* translators: %s is the option name */
747
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
748
				break;
749
			case 'update':
750
				jetpack_cli_are_you_sure( $flagged );
751
752
				// Updating arrays would get pretty tricky...
753
				$value = Jetpack_Options::get_option( $args[1] );
754
				if ( $value && is_array( $value ) ) {
755
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
756
				}
757
758
				Jetpack_Options::update_option( $args[1], $args[2] );
759
				/* translators: %1$s is the previous value, %2$s is the new value */
760
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
761
				break;
762
			case 'list':
763
				$options_compact     = Jetpack_Options::get_option_names();
764
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
765
				$options_private     = Jetpack_Options::get_option_names( 'private' );
766
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
767
768
				// Table headers
769
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
770
771
				// List out the options and their values
772
				// Tell them if the value is empty or not
773
				// Tell them if it's an array
774
				foreach ( $options as $option ) {
775
					$value = Jetpack_Options::get_option( $option );
776
					if ( ! $value ) {
777
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
778
						continue;
779
					}
780
781
					if ( ! is_array( $value ) ) {
782
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
783
					} else if ( is_array( $value ) ) {
784
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
785
					}
786
				}
787
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
788
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
789
790
				WP_CLI::success(
791
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
792
					str_pad( 'wp jetpack options get', 26 )    . $option_text . "\n" .
793
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
794
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
795
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
796
				);
797
				break;
798
		}
799
	}
800
801
	/**
802
	 * Get the status of or start a new Jetpack sync.
803
	 *
804
	 * ## OPTIONS
805
	 *
806
	 * status   : Print the current sync status
807
	 * settings : Prints the current sync settings
808
	 * start    : Start a full sync from this site to WordPress.com
809
	 * enable   : Enables sync on the site
810
	 * disable  : Disable sync on a site
811
	 * reset    : Disables sync and Resets the sync queues on a site
812
	 *
813
	 * ## EXAMPLES
814
	 *
815
	 * wp jetpack sync status
816
	 * wp jetpack sync settings
817
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
818
	 * wp jetpack sync enable
819
	 * wp jetpack sync disable
820
	 * wp jetpack sync reset
821
	 * wp jetpack sync reset --queue=full or regular
822
	 *
823
	 * @synopsis <status|start> [--<field>=<value>]
824
	 */
825
	public function sync( $args, $assoc_args ) {
826
827
		$action = isset( $args[0] ) ? $args[0] : 'status';
828
829
		switch ( $action ) {
830
			case 'status':
831
				$status = Actions::get_sync_status();
832
				$collection = array();
833
				foreach ( $status as $key => $item ) {
834
					$collection[]  = array(
835
						'option' => $key,
836
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
837
					);
838
				}
839
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
840
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
841
				break;
842
			case 'settings':
843
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
844
				foreach( Settings::get_settings() as $setting => $item ) {
845
					$settings[]  = array(
846
						'setting' => $setting,
847
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
848
					);
849
				}
850
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
851
852
			case 'disable':
853
				// Don't set it via the Settings since that also resets the queues.
854
				update_option( 'jetpack_sync_settings_disable', 1 );
855
				/* translators: %s is the site URL */
856
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
857
				break;
858
			case 'enable':
859
				Settings::update_settings( array( 'disable' => 0 ) );
860
				/* translators: %s is the site URL */
861
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
862
				break;
863
			case 'reset':
864
				// Don't set it via the Settings since that also resets the queues.
865
				update_option( 'jetpack_sync_settings_disable', 1 );
866
867
				/* translators: %s is the site URL */
868
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
869
				$listener = Listener::get_instance();
870
				if ( empty( $assoc_args['queue'] ) ) {
871
					$listener->get_sync_queue()->reset();
872
					$listener->get_full_sync_queue()->reset();
873
					/* translators: %s is the site URL */
874
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
875
					break;
876
				}
877
878
				if ( ! empty( $assoc_args['queue'] ) ) {
879
					switch ( $assoc_args['queue'] ) {
880 View Code Duplication
						case 'regular':
881
							$listener->get_sync_queue()->reset();
882
							/* translators: %s is the site URL */
883
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
884
							break;
885 View Code Duplication
						case 'full':
886
							$listener->get_full_sync_queue()->reset();
887
							/* translators: %s is the site URL */
888
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
889
							break;
890
						default:
891
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
892
							break;
893
					}
894
				}
895
896
				break;
897
			case 'start':
898
				if ( ! Actions::sync_allowed() ) {
899
					if( ! Settings::get_setting( 'disable' ) ) {
900
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. It is currently disabled. Run `wp jetpack sync enable` to enable it.', 'jetpack' ) );
901
						return;
902
					}
903
					if ( doing_action( 'jetpack_user_authorized' ) || Jetpack::is_active() ) {
904
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
905
						return;
906
					}
907
					if ( Jetpack::is_development_mode() ) {
908
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in development mode.', 'jetpack' ) );
909
						return;
910
					}
911
					if (  Jetpack::is_staging_site() ) {
912
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in staging mode.', 'jetpack' ) );
913
						return;
914
					}
915
916
				}
917
				// Get the original settings so that we can restore them later
918
				$original_settings = Settings::get_settings();
919
920
				// Initialize sync settigns so we can sync as quickly as possible
921
				$sync_settings = wp_parse_args(
922
					array_intersect_key( $assoc_args, Settings::$valid_settings ),
0 ignored issues
show
The property valid_settings cannot be accessed from this context as it is declared private in class Automattic\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...
923
					array(
924
						'sync_wait_time' => 0,
925
						'enqueue_wait_time' => 0,
926
						'queue_max_writes_sec' => 10000,
927
						'max_queue_size_full_sync' => 100000
928
					)
929
				);
930
				Settings::update_settings( $sync_settings );
931
932
				// Convert comma-delimited string of modules to an array
933 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
934
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
935
936
					// Convert the array so that the keys are the module name and the value is true to indicate
937
					// that we want to sync the module
938
					$modules = array_map( '__return_true', array_flip( $modules ) );
939
				}
940
941 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
942
					if (
943
						'users' === $module_name &&
944
						isset( $assoc_args[ $module_name ] ) &&
945
						'initial' === $assoc_args[ $module_name ]
946
					) {
947
						$modules[ 'users' ] = 'initial';
948
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
949
						$ids = explode( ',', $assoc_args[ $module_name ] );
950
						if ( count( $ids ) > 0 ) {
951
							$modules[ $module_name ] = $ids;
952
						}
953
					}
954
				}
955
956
				if ( empty( $modules ) ) {
957
					$modules = null;
958
				}
959
960
				// Kick off a full sync
961
				if ( Actions::do_full_sync( $modules ) ) {
962
					if ( $modules ) {
963
						/* translators: %s is a comma separated list of Jetpack modules */
964
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
965
					} else {
966
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
967
					}
968 View Code Duplication
				} else {
969
970
					// Reset sync settings to original.
971
					Settings::update_settings( $original_settings );
972
973
					if ( $modules ) {
974
						/* translators: %s is a comma separated list of Jetpack modules */
975
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
976
					} else {
977
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
978
					}
979
				}
980
981
				// Keep sending to WPCOM until there's nothing to send
982
				$i = 1;
983
				do {
984
					$result = Actions::$sender->do_full_sync();
0 ignored issues
show
The property sender cannot be accessed from this context as it is declared private in class Automattic\Jetpack\Sync\Actions.

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