Completed
Push — branch-testing-comp ( 8aeb14 )
by Jeremy
07:22
created

class.jetpack-cli.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
WP_CLI::add_command( 'jetpack', 'Jetpack_CLI' );
4
5
use Automattic\Jetpack\Connection\Client;
6
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
7
use Automattic\Jetpack\Sync\Actions;
8
use Automattic\Jetpack\Sync\Listener;
9
use Automattic\Jetpack\Sync\Queue;
10
use Automattic\Jetpack\Sync\Settings;
11
12
/**
13
 * Control your local Jetpack installation.
14
 *
15
 * Minimum PHP requirement for WP-CLI is PHP 5.3, so ignore PHP 5.2 compatibility issues.
16
 * @phpcs:disable PHPCompatibility.PHP.NewLanguageConstructs.t_ns_separatorFound
17
 */
18
class Jetpack_CLI extends WP_CLI_Command {
19
	// Aesthetics
20
	public $green_open  = "\033[32m";
21
	public $red_open    = "\033[31m";
22
	public $yellow_open = "\033[33m";
23
	public $color_close = "\033[0m";
24
25
	/**
26
	 * Get Jetpack Details
27
	 *
28
	 * ## OPTIONS
29
	 *
30
	 * empty: Leave it empty for basic stats
31
	 *
32
	 * full: View full stats.  It's the data from the heartbeat
33
	 *
34
	 * ## EXAMPLES
35
	 *
36
	 * wp jetpack status
37
	 * wp jetpack status full
38
	 */
39
	public function status( $args, $assoc_args ) {
40
		jetpack_require_lib( 'debugger' );
41
42
		/* translators: %s is the site URL */
43
		WP_CLI::line( sprintf( __( 'Checking status for %s', 'jetpack' ), esc_url( get_home_url() ) ) );
44
45 View Code Duplication
		if ( isset( $args[0] ) && 'full' !== $args[0] ) {
46
			/* translators: %s is a command like "prompt" */
47
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $args[0] ) );
48
		}
49
50
		$master_user_email = Jetpack::get_master_user_email();
51
52
		$cxntests = new Jetpack_Cxn_Tests();
53
54
		if ( $cxntests->pass() ) {
55
			$cxntests->output_results_for_cli();
56
57
			WP_CLI::success( __( 'Jetpack is currently connected to WordPress.com', 'jetpack' ) );
58
		} else {
59
			$error = array();
60
			foreach ( $cxntests->list_fails() as $fail ) {
61
				$error[] = $fail['name'] . ': ' . $fail['message'];
62
			}
63
			WP_CLI::error_multi_line( $error );
64
65
			$cxntests->output_results_for_cli();
66
67
			WP_CLI::error( __( 'Jetpack connection is broken.', 'jetpack' ) ); // Exit CLI.
68
		}
69
70
		/* translators: %s is current version of Jetpack, for example 7.3 */
71
		WP_CLI::line( sprintf( __( 'The Jetpack Version is %s', 'jetpack' ), JETPACK__VERSION ) );
72
		/* translators: %d is WP.com ID of this blog */
73
		WP_CLI::line( sprintf( __( 'The WordPress.com blog_id is %d', 'jetpack' ), Jetpack_Options::get_option( 'id' ) ) );
74
		/* translators: %s is the email address of the connection owner */
75
		WP_CLI::line( sprintf( __( 'The WordPress.com account for the primary connection is %s', 'jetpack' ), $master_user_email ) );
76
77
		/*
78
		 * Are they asking for all data?
79
		 *
80
		 * Loop through heartbeat data and organize by priority.
81
		 */
82
		$all_data = ( isset( $args[0] ) && 'full' == $args[0] ) ? 'full' : false;
83
		if ( $all_data ) {
84
			// Heartbeat data
85
			WP_CLI::line( "\n" . __( 'Additional data: ', 'jetpack' ) );
86
87
			// Get the filtered heartbeat data.
88
			// Filtered so we can color/list by severity
89
			$stats = Jetpack::jetpack_check_heartbeat_data();
90
91
			// Display red flags first
92
			foreach ( $stats['bad'] as $stat => $value ) {
93
				printf( "$this->red_open%-'.16s %s $this->color_close\n", $stat, $value );
94
			}
95
96
			// Display caution warnings next
97
			foreach ( $stats['caution'] as $stat => $value ) {
98
				printf( "$this->yellow_open%-'.16s %s $this->color_close\n", $stat, $value );
99
			}
100
101
			// The rest of the results are good!
102
			foreach ( $stats['good'] as $stat => $value ) {
103
104
				// Modules should get special spacing for aestetics
105
				if ( strpos( $stat, 'odule-' ) ) {
106
					printf( "%-'.30s %s\n", $stat, $value );
107
					usleep( 4000 ); // For dramatic effect lolz
108
					continue;
109
				}
110
				printf( "%-'.16s %s\n", $stat, $value );
111
				usleep( 4000 ); // For dramatic effect lolz
112
			}
113
		} else {
114
			// Just the basics
115
			WP_CLI::line( "\n" . _x( "View full status with 'wp jetpack status full'", '"wp jetpack status full" is a command - do not translate', 'jetpack' ) );
116
		}
117
	}
118
119
	/**
120
	 * Tests the active connection
121
	 *
122
	 * 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.
123
	 *
124
	 * ## EXAMPLES
125
	 *
126
	 * wp jetpack test-connection
127
	 *
128
	 * @subcommand test-connection
129
	 */
130
	public function test_connection( $args, $assoc_args ) {
131
132
		/* translators: %s is the site URL */
133
		WP_CLI::line( sprintf( __( 'Testing connection for %s', 'jetpack' ), esc_url( get_site_url() ) ) );
134
135
		if ( ! Jetpack::is_active() ) {
136
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
137
		}
138
139
		$response = Client::wpcom_json_api_request_as_blog(
140
			sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
141
			Client::WPCOM_JSON_API_VERSION
142
		);
143
144 View Code Duplication
		if ( is_wp_error( $response ) ) {
145
			/* translators: %1$s is the error code, %2$s is the error message */
146
			WP_CLI::error( sprintf( __( 'Failed to test connection (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() ) );
147
		}
148
149
		$body = wp_remote_retrieve_body( $response );
150
		if ( ! $body ) {
151
			WP_CLI::error( __( 'Failed to test connection (empty response body)', 'jetpack' ) );
152
		}
153
154
		$result       = json_decode( $body );
155
		$is_connected = (bool) $result->connected;
156
		$message      = $result->message;
157
158
		if ( $is_connected ) {
159
			WP_CLI::success( $message );
160
		} else {
161
			WP_CLI::error( $message );
162
		}
163
	}
164
165
	/**
166
	 * Disconnect Jetpack Blogs or Users
167
	 *
168
	 * ## OPTIONS
169
	 *
170
	 * blog: Disconnect the entire blog.
171
	 *
172
	 * user <user_identifier>: Disconnect a specific user from WordPress.com.
173
	 *
174
	 * Please note, the primary account that the blog is connected
175
	 * to WordPress.com with cannot be disconnected without
176
	 * disconnecting the entire blog.
177
	 *
178
	 * ## EXAMPLES
179
	 *
180
	 * wp jetpack disconnect blog
181
	 * wp jetpack disconnect user 13
182
	 * wp jetpack disconnect user username
183
	 * wp jetpack disconnect user [email protected]
184
	 *
185
	 * @synopsis <blog|user> [<user_identifier>]
186
	 */
187
	public function disconnect( $args, $assoc_args ) {
188
		if ( ! Jetpack::is_active() ) {
189
			WP_CLI::error( __( 'You cannot disconnect, without having first connected.', 'jetpack' ) );
190
		}
191
192
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
193
		if ( ! in_array( $action, array( 'blog', 'user', 'prompt' ) ) ) {
194
			/* translators: %s is a command like "prompt" */
195
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
196
		}
197
198
		if ( in_array( $action, array( 'user' ) ) ) {
199
			if ( isset( $args[1] ) ) {
200
				$user_id = $args[1];
201
				if ( ctype_digit( $user_id ) ) {
202
					$field   = 'id';
203
					$user_id = (int) $user_id;
204
				} elseif ( is_email( $user_id ) ) {
205
					$field   = 'email';
206
					$user_id = sanitize_user( $user_id, true );
207
				} else {
208
					$field   = 'login';
209
					$user_id = sanitize_user( $user_id, true );
210
				}
211
				if ( ! $user = get_user_by( $field, $user_id ) ) {
212
					WP_CLI::error( __( 'Please specify a valid user.', 'jetpack' ) );
213
				}
214
			} else {
215
				WP_CLI::error( __( 'Please specify a user by either ID, username, or email.', 'jetpack' ) );
216
			}
217
		}
218
219
		switch ( $action ) {
220
			case 'blog':
221
				Jetpack::log( 'disconnect' );
222
				Jetpack::disconnect();
223
				WP_CLI::success(
224
					sprintf(
225
						/* translators: %s is the site URL */
226
						__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
227
						esc_url( get_site_url() )
228
					)
229
				);
230
				break;
231
			case 'user':
232
				if ( Connection_Manager::disconnect_user( $user->ID ) ) {
233
					Jetpack::log( 'unlink', $user->ID );
234
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
235
				} else {
236
					/* translators: %s is a username */
237
					WP_CLI::error( sprintf( __( "User %s could not be disconnected. Are you sure they're connected currently?", 'jetpack' ), "{$user->login} <{$user->email}>" ) );
238
				}
239
				break;
240
			case 'prompt':
241
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
242
				break;
243
		}
244
	}
245
246
	/**
247
	 * Reset Jetpack options and settings to default
248
	 *
249
	 * ## OPTIONS
250
	 *
251
	 * modules: Resets modules to default state ( get_default_modules() )
252
	 *
253
	 * options: Resets all Jetpack options except:
254
	 *  - All private options (Blog token, user token, etc...)
255
	 *  - id (The Client ID/WP.com Blog ID of this site)
256
	 *  - master_user
257
	 *  - version
258
	 *  - activated
259
	 *
260
	 * ## EXAMPLES
261
	 *
262
	 * wp jetpack reset options
263
	 * wp jetpack reset modules
264
	 * wp jetpack reset sync-checksum --dry-run --offset=0
265
	 *
266
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
267
	 */
268
	public function reset( $args, $assoc_args ) {
269
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
270 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
271
			/* translators: %s is a command like "prompt" */
272
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
273
		}
274
275
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
276
277 View Code Duplication
		if ( $is_dry_run ) {
278
			WP_CLI::warning(
279
				__( "\nThis is a dry run.\n", 'jetpack' ) .
280
				__( "No actions will be taken.\n", 'jetpack' ) .
281
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
282
			);
283
		} else {
284
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
285
			jetpack_cli_are_you_sure();
286
		}
287
288
		switch ( $action ) {
289
			case 'options':
290
				$options_to_reset = Jetpack_Options::get_options_for_reset();
291
				// Reset the Jetpack options
292
				WP_CLI::line(
293
					sprintf(
294
						/* translators: %s is the site URL */
295
						__( "Resetting Jetpack Options for %s...\n", 'jetpack' ),
296
						esc_url( get_site_url() )
297
					)
298
				);
299
				sleep( 1 ); // Take a breath
300 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
301
					if ( ! $is_dry_run ) {
302
						Jetpack_Options::delete_option( $option_to_reset );
303
						usleep( 100000 );
304
					}
305
306
					/* translators: This is the result of an action. The option named %s was reset */
307
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
308
				}
309
310
				// Reset the WP options
311
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", 'jetpack' ) );
312
				usleep( 500000 ); // Take a breath
313 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
314
					if ( ! $is_dry_run ) {
315
						delete_option( $option_to_reset );
316
						usleep( 100000 );
317
					}
318
					/* translators: This is the result of an action. The option named %s was reset */
319
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
320
				}
321
322
				// Reset to default modules
323
				WP_CLI::line( __( "Resetting default modules...\n", 'jetpack' ) );
324
				usleep( 500000 ); // Take a breath
325
				$default_modules = Jetpack::get_default_modules();
326
				if ( ! $is_dry_run ) {
327
					Jetpack::update_active_modules( $default_modules );
328
				}
329
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
330
				break;
331 View Code Duplication
			case 'modules':
332
				if ( ! $is_dry_run ) {
333
					$default_modules = Jetpack::get_default_modules();
334
					Jetpack::update_active_modules( $default_modules );
335
				}
336
337
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
338
				break;
339
			case 'prompt':
340
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
341
				break;
342
			case 'sync-checksum':
343
				$option = 'jetpack_callables_sync_checksum';
344
345
				if ( is_multisite() ) {
346
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
347
348
					/*
349
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
350
					 * Offset can be used to paginate and try to clean up more sites.
351
					 */
352
					$sites       = get_sites(
353
						array(
354
							'number' => 1000,
355
							'offset' => $offset,
356
						)
357
					);
358
					$count_fixes = 0;
359
					foreach ( $sites as $site ) {
360
						switch_to_blog( $site->blog_id );
361
						$count = self::count_option( $option );
362
						if ( $count > 1 ) {
363
							if ( ! $is_dry_run ) {
364
								delete_option( $option );
365
							}
366
							WP_CLI::line(
367
								sprintf(
368
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
369
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
370
									$count,
371
									$option,
372
									"{$site->domain}{$site->path}"
373
								)
374
							);
375
							$count_fixes++;
376
							if ( ! $is_dry_run ) {
377
								/*
378
								 * We could be deleting a lot of options rows at the same time.
379
								 * Allow some time for replication to catch up.
380
								 */
381
								sleep( 3 );
382
							}
383
						}
384
385
						restore_current_blog();
386
					}
387
					if ( $count_fixes ) {
388
						WP_CLI::success(
389
							sprintf(
390
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
391
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
392
								$option,
393
								$count_fixes
394
							)
395
						);
396
					} else {
397
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
398
					}
399
					return;
400
				}
401
402
				$count = self::count_option( $option );
403
				if ( $count > 1 ) {
404
					if ( ! $is_dry_run ) {
405
						delete_option( $option );
406
					}
407
					WP_CLI::success(
408
						sprintf(
409
							/* translators: %1$d is a number, %2$s is the name of an option. */
410
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
411
							$count,
412
							$option
413
						)
414
					);
415
					return;
416
				}
417
418
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
419
				break;
420
421
		}
422
	}
423
424
	/**
425
	 * Return the number of times an option appears
426
	 * Normally an option would only appear 1 since the option key is supposed to be unique
427
	 * but if a site hasn't updated the DB schema then that would not be the case.
428
	 *
429
	 * @param string $option Option name.
430
	 *
431
	 * @return int
432
	 */
433
	private static function count_option( $option ) {
434
		global $wpdb;
435
		return (int) $wpdb->get_var(
436
			$wpdb->prepare(
437
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
438
				$option
439
			)
440
		);
441
442
	}
443
444
	/**
445
	 * Manage Jetpack Modules
446
	 *
447
	 * ## OPTIONS
448
	 *
449
	 * <list|activate|deactivate|toggle>
450
	 * : The action to take.
451
	 * ---
452
	 * default: list
453
	 * options:
454
	 *  - list
455
	 *  - activate
456
	 *  - deactivate
457
	 *  - toggle
458
	 * ---
459
	 *
460
	 * [<module_slug>]
461
	 * : The slug of the module to perform an action on.
462
	 *
463
	 * [--format=<format>]
464
	 * : Allows overriding the output of the command when listing modules.
465
	 * ---
466
	 * default: table
467
	 * options:
468
	 *  - table
469
	 *  - json
470
	 *  - csv
471
	 *  - yaml
472
	 *  - ids
473
	 *  - count
474
	 * ---
475
	 *
476
	 * ## EXAMPLES
477
	 *
478
	 * wp jetpack module list
479
	 * wp jetpack module list --format=json
480
	 * wp jetpack module activate stats
481
	 * wp jetpack module deactivate stats
482
	 * wp jetpack module toggle stats
483
	 * wp jetpack module activate all
484
	 * wp jetpack module deactivate all
485
	 */
486
	public function module( $args, $assoc_args ) {
487
		$action = isset( $args[0] ) ? $args[0] : 'list';
488
489
		if ( isset( $args[1] ) ) {
490
			$module_slug = $args[1];
491
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
492
				/* translators: %s is a module slug like "stats" */
493
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
494
			}
495
			if ( 'toggle' === $action ) {
496
				$action = Jetpack::is_module_active( $module_slug )
497
					? 'deactivate'
498
					: 'activate';
499
			}
500
			if ( 'all' === $args[1] ) {
501
				$action = ( 'deactivate' === $action )
502
					? 'deactivate_all'
503
					: 'activate_all';
504
			}
505
		} elseif ( 'list' !== $action ) {
506
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
507
			$action = 'list';
508
		}
509
510
		switch ( $action ) {
511
			case 'list':
512
				$modules_list = array();
513
				$modules      = Jetpack::get_available_modules();
514
				sort( $modules );
515
				foreach ( (array) $modules as $module_slug ) {
516
					if ( 'vaultpress' === $module_slug ) {
517
						continue;
518
					}
519
					$modules_list[] = array(
520
						'slug'   => $module_slug,
521
						'status' => Jetpack::is_module_active( $module_slug )
522
							? __( 'Active', 'jetpack' )
523
							: __( 'Inactive', 'jetpack' ),
524
					);
525
				}
526
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
527
				break;
528
			case 'activate':
529
				$module = Jetpack::get_module( $module_slug );
530
				Jetpack::log( 'activate', $module_slug );
531
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
532
					/* translators: %s is the name of a Jetpack module */
533
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
534
				} else {
535
					/* translators: %s is the name of a Jetpack module */
536
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
537
				}
538
				break;
539 View Code Duplication
			case 'activate_all':
540
				$modules = Jetpack::get_available_modules();
541
				Jetpack::update_active_modules( $modules );
542
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
543
				break;
544
			case 'deactivate':
545
				$module = Jetpack::get_module( $module_slug );
546
				Jetpack::log( 'deactivate', $module_slug );
547
				Jetpack::deactivate_module( $module_slug );
548
				/* translators: %s is the name of a Jetpack module */
549
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
550
				break;
551
			case 'deactivate_all':
552
				Jetpack::delete_active_modules();
553
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
554
				break;
555
			case 'toggle':
556
				// Will never happen, should have been handled above and changed to activate or deactivate.
557
				break;
558
		}
559
	}
560
561
	/**
562
	 * Manage Protect Settings
563
	 *
564
	 * ## OPTIONS
565
	 *
566
	 * whitelist: Whitelist an IP address.  You can also read or clear the whitelist.
567
	 *
568
	 *
569
	 * ## EXAMPLES
570
	 *
571
	 * wp jetpack protect whitelist <ip address>
572
	 * wp jetpack protect whitelist list
573
	 * wp jetpack protect whitelist clear
574
	 *
575
	 * @synopsis <whitelist> [<ip|ip_low-ip_high|list|clear>]
576
	 */
577
	public function protect( $args, $assoc_args ) {
578
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
579
		if ( ! in_array( $action, array( 'whitelist' ) ) ) {
580
			/* translators: %s is a command like "prompt" */
581
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
582
		}
583
		// Check if module is active
584
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
585
			/* translators: %s is a module name */
586
			WP_CLI::error( sprintf( _x( '%1$s is not active. You can activate it with "wp jetpack module activate %2$s"', '"wp jetpack module activate" is a command - do not translate', 'jetpack' ), __FUNCTION__, __FUNCTION__ ) );
587
		}
588
		if ( in_array( $action, array( 'whitelist' ) ) ) {
589
			if ( isset( $args[1] ) ) {
590
				$action = 'whitelist';
591
			} else {
592
				$action = 'prompt';
593
			}
594
		}
595
		switch ( $action ) {
596
			case 'whitelist':
597
				$whitelist         = array();
598
				$new_ip            = $args[1];
599
				$current_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
600
601
				// Build array of IPs that are already whitelisted.
602
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
603
				// low & high range params for jetpack_protect_ip_address_is_in_range();
604
				foreach ( $current_whitelist as $whitelisted ) {
605
606
					// IP ranges
607
					if ( $whitelisted->range ) {
608
609
						// Is it already whitelisted?
610
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $whitelisted->range_low, $whitelisted->range_high ) ) {
611
							/* translators: %s is an IP address */
612
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
613
							break;
614
						}
615
						$whitelist[] = $whitelisted->range_low . ' - ' . $whitelisted->range_high;
616
617
					} else { // Individual IPs
618
619
						// Check if the IP is already whitelisted (single IP only)
620
						if ( $new_ip == $whitelisted->ip_address ) {
621
							/* translators: %s is an IP address */
622
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
623
							break;
624
						}
625
						$whitelist[] = $whitelisted->ip_address;
626
627
					}
628
				}
629
630
				/*
631
				 * List the whitelist
632
				 * Done here because it's easier to read the $whitelist array after it's been rebuilt
633
				 */
634
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
635
					if ( ! empty( $whitelist ) ) {
636
						WP_CLI::success( __( 'Here are your whitelisted IPs:', 'jetpack' ) );
637
						foreach ( $whitelist as $ip ) {
638
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) );
639
						}
640
					} else {
641
						WP_CLI::line( __( 'Whitelist is empty.', 'jetpack' ) );
642
					}
643
					break;
644
				}
645
646
				/*
647
				 * Clear the whitelist
648
				 */
649
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
650
					if ( ! empty( $whitelist ) ) {
651
						$whitelist = array();
652
						jetpack_protect_save_whitelist( $whitelist );
653
						WP_CLI::success( __( 'Cleared all whitelisted IPs', 'jetpack' ) );
654
					} else {
655
						WP_CLI::line( __( 'Whitelist is empty.', 'jetpack' ) );
656
					}
657
					break;
658
				}
659
660
				// Append new IP to whitelist array
661
				array_push( $whitelist, $new_ip );
662
663
				// Save whitelist if there are no errors
664
				$result = jetpack_protect_save_whitelist( $whitelist );
665
				if ( is_wp_error( $result ) ) {
666
					WP_CLI::error( $result );
667
				}
668
669
				/* translators: %s is an IP address */
670
				WP_CLI::success( sprintf( __( '%s has been whitelisted.', 'jetpack' ), $new_ip ) );
671
				break;
672
			case 'prompt':
673
				WP_CLI::error(
674
					__( 'No command found.', 'jetpack' ) . "\n" .
675
					__( 'Please enter the IP address you want to whitelist.', 'jetpack' ) . "\n" .
676
					_x( 'You can save a range of IPs {low_range}-{high_range}. No spaces allowed.  (example: 1.1.1.1-2.2.2.2)', 'Instructions on how to whitelist IP ranges - low_range/high_range should be translated.', 'jetpack' ) . "\n" .
677
					_x( "You can also 'list' or 'clear' the whitelist.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
678
				);
679
				break;
680
		}
681
	}
682
683
	/**
684
	 * Manage Jetpack Options
685
	 *
686
	 * ## OPTIONS
687
	 *
688
	 * list   : List all jetpack options and their values
689
	 * delete : Delete an option
690
	 *          - can only delete options that are white listed.
691
	 * update : update an option
692
	 *          - can only update option strings
693
	 * get    : get the value of an option
694
	 *
695
	 * ## EXAMPLES
696
	 *
697
	 * wp jetpack options list
698
	 * wp jetpack options get    <option_name>
699
	 * wp jetpack options delete <option_name>
700
	 * wp jetpack options update <option_name> [<option_value>]
701
	 *
702
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
703
	 */
704
	public function options( $args, $assoc_args ) {
705
		$action         = isset( $args[0] ) ? $args[0] : 'list';
706
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
707
708
		// Is the option flagged as unsafe?
709
		$flagged = ! in_array( $args[1], $safe_to_modify );
710
711 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
712
			/* translators: %s is a command like "prompt" */
713
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
714
		}
715
716
		if ( isset( $args[0] ) ) {
717
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
718
				$action = 'get';
719
			} elseif ( 'delete' == $args[0] && isset( $args[1] ) ) {
720
				$action = 'delete';
721
			} elseif ( 'update' == $args[0] && isset( $args[1] ) ) {
722
				$action = 'update';
723
			} else {
724
				$action = 'list';
725
			}
726
		}
727
728
		// Bail if the option isn't found
729
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
730 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
731
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
732
		}
733
734
		// Let's print_r the option if it's an array
735
		// Used in the 'get' and 'list' actions
736
		$option = is_array( $option ) ? print_r( $option ) : $option;
737
738
		switch ( $action ) {
739
			case 'get':
740
				WP_CLI::success( "\t" . $option );
741
				break;
742
			case 'delete':
743
				jetpack_cli_are_you_sure( $flagged );
744
745
				Jetpack_Options::delete_option( $args[1] );
746
				/* translators: %s is the option name */
747
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
748
				break;
749
			case 'update':
750
				jetpack_cli_are_you_sure( $flagged );
751
752
				// Updating arrays would get pretty tricky...
753
				$value = Jetpack_Options::get_option( $args[1] );
754
				if ( $value && is_array( $value ) ) {
755
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
756
				}
757
758
				Jetpack_Options::update_option( $args[1], $args[2] );
759
				/* translators: %1$s is the previous value, %2$s is the new value */
760
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
761
				break;
762
			case 'list':
763
				$options_compact     = Jetpack_Options::get_option_names();
764
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
765
				$options_private     = Jetpack_Options::get_option_names( 'private' );
766
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
767
768
				// Table headers
769
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
770
771
				// List out the options and their values
772
				// Tell them if the value is empty or not
773
				// Tell them if it's an array
774
				foreach ( $options as $option ) {
775
					$value = Jetpack_Options::get_option( $option );
776
					if ( ! $value ) {
777
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
778
						continue;
779
					}
780
781
					if ( ! is_array( $value ) ) {
782
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
783
					} elseif ( is_array( $value ) ) {
784
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
785
					}
786
				}
787
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
788
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
789
790
				WP_CLI::success(
791
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
792
					str_pad( 'wp jetpack options get', 26 ) . $option_text . "\n" .
793
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
794
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
795
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
796
				);
797
				break;
798
		}
799
	}
800
801
	/**
802
	 * Get the status of or start a new Jetpack sync.
803
	 *
804
	 * ## OPTIONS
805
	 *
806
	 * status   : Print the current sync status
807
	 * settings : Prints the current sync settings
808
	 * start    : Start a full sync from this site to WordPress.com
809
	 * enable   : Enables sync on the site
810
	 * disable  : Disable sync on a site
811
	 * reset    : Disables sync and Resets the sync queues on a site
812
	 *
813
	 * ## EXAMPLES
814
	 *
815
	 * wp jetpack sync status
816
	 * wp jetpack sync settings
817
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
818
	 * wp jetpack sync enable
819
	 * wp jetpack sync disable
820
	 * wp jetpack sync reset
821
	 * wp jetpack sync reset --queue=full or regular
822
	 *
823
	 * @synopsis <status|start> [--<field>=<value>]
824
	 */
825
	public function sync( $args, $assoc_args ) {
826
827
		$action = isset( $args[0] ) ? $args[0] : 'status';
828
829
		switch ( $action ) {
830
			case 'status':
831
				$status     = Actions::get_sync_status();
832
				$collection = array();
833
				foreach ( $status as $key => $item ) {
834
					$collection[] = array(
835
						'option' => $key,
836
						'value'  => is_scalar( $item ) ? $item : json_encode( $item ),
837
					);
838
				}
839
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
840
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
841
				break;
842
			case 'settings':
843
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
844
				foreach ( Settings::get_settings() as $setting => $item ) {
845
					$settings[] = array(
846
						'setting' => $setting,
847
						'value'   => is_scalar( $item ) ? $item : json_encode( $item ),
848
					);
849
				}
850
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
851
852
			case 'disable':
853
				// Don't set it via the Settings since that also resets the queues.
854
				update_option( 'jetpack_sync_settings_disable', 1 );
855
				/* translators: %s is the site URL */
856
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
857
				break;
858
			case 'enable':
859
				Settings::update_settings( array( 'disable' => 0 ) );
860
				/* translators: %s is the site URL */
861
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
862
				break;
863
			case 'reset':
864
				// Don't set it via the Settings since that also resets the queues.
865
				update_option( 'jetpack_sync_settings_disable', 1 );
866
867
				/* translators: %s is the site URL */
868
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
869
				$listener = Listener::get_instance();
870
				if ( empty( $assoc_args['queue'] ) ) {
871
					$listener->get_sync_queue()->reset();
872
					$listener->get_full_sync_queue()->reset();
873
					/* translators: %s is the site URL */
874
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
875
					break;
876
				}
877
878
				if ( ! empty( $assoc_args['queue'] ) ) {
879
					switch ( $assoc_args['queue'] ) {
880 View Code Duplication
						case 'regular':
881
							$listener->get_sync_queue()->reset();
882
							/* translators: %s is the site URL */
883
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
884
							break;
885 View Code Duplication
						case 'full':
886
							$listener->get_full_sync_queue()->reset();
887
							/* translators: %s is the site URL */
888
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
889
							break;
890
						default:
891
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
892
							break;
893
					}
894
				}
895
896
				break;
897
			case 'start':
898
				if ( ! Actions::sync_allowed() ) {
899
					if ( ! Settings::get_setting( 'disable' ) ) {
900
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. It is currently disabled. Run `wp jetpack sync enable` to enable it.', 'jetpack' ) );
901
						return;
902
					}
903
					if ( doing_action( 'jetpack_user_authorized' ) || Jetpack::is_active() ) {
904
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
905
						return;
906
					}
907
					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
				// Get the original settings so that we can restore them later
917
				$original_settings = Settings::get_settings();
918
919
				// Initialize sync settigns so we can sync as quickly as possible
920
				$sync_settings = wp_parse_args(
921
					array_intersect_key( $assoc_args, Settings::$valid_settings ),
922
					array(
923
						'sync_wait_time'           => 0,
924
						'enqueue_wait_time'        => 0,
925
						'queue_max_writes_sec'     => 10000,
926
						'max_queue_size_full_sync' => 100000,
927
					)
928
				);
929
				Settings::update_settings( $sync_settings );
930
931
				// Convert comma-delimited string of modules to an array
932 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
933
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
934
935
					// Convert the array so that the keys are the module name and the value is true to indicate
936
					// that we want to sync the module
937
					$modules = array_map( '__return_true', array_flip( $modules ) );
938
				}
939
940 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
941
					if (
942
						'users' === $module_name &&
943
						isset( $assoc_args[ $module_name ] ) &&
944
						'initial' === $assoc_args[ $module_name ]
945
					) {
946
						$modules['users'] = 'initial';
947
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
948
						$ids = explode( ',', $assoc_args[ $module_name ] );
949
						if ( count( $ids ) > 0 ) {
950
							$modules[ $module_name ] = $ids;
951
						}
952
					}
953
				}
954
955
				if ( empty( $modules ) ) {
956
					$modules = null;
957
				}
958
959
				// Kick off a full sync
960
				if ( Actions::do_full_sync( $modules ) ) {
961
					if ( $modules ) {
962
						/* translators: %s is a comma separated list of Jetpack modules */
963
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
964
					} else {
965
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
966
					}
967 View Code Duplication
				} else {
968
969
					// Reset sync settings to original.
970
					Settings::update_settings( $original_settings );
971
972
					if ( $modules ) {
973
						/* translators: %s is a comma separated list of Jetpack modules */
974
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
975
					} else {
976
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
977
					}
978
				}
979
980
				// Keep sending to WPCOM until there's nothing to send
981
				$i = 1;
982
				do {
983
					$result = Actions::$sender->do_full_sync();
984
					if ( is_wp_error( $result ) ) {
985
						$queue_empty_error = ( 'empty_queue_full_sync' == $result->get_error_code() );
986
						if ( ! $queue_empty_error || ( $queue_empty_error && ( 1 == $i ) ) ) {
987
							/* translators: %s is an error code  */
988
							WP_CLI::error( sprintf( __( 'Sync errored with code: %s', 'jetpack' ), $result->get_error_code() ) );
989
						}
990
					} else {
991
						if ( 1 == $i ) {
992
							WP_CLI::log( __( 'Sent data to WordPress.com', 'jetpack' ) );
993
						} else {
994
							WP_CLI::log( __( 'Sent more data to WordPress.com', 'jetpack' ) );
995
						}
996
					}
997
					$i++;
998
				} while ( $result && ! is_wp_error( $result ) );
999
1000
				// Reset sync settings to original.
1001
				Settings::update_settings( $original_settings );
1002
1003
				WP_CLI::success( __( 'Finished syncing to WordPress.com', 'jetpack' ) );
1004
				break;
1005
		}
1006
	}
1007
1008
	/**
1009
	 * List the contents of a specific Jetpack sync queue.
1010
	 *
1011
	 * ## OPTIONS
1012
	 *
1013
	 * peek : List the 100 front-most items on the queue.
1014
	 *
1015
	 * ## EXAMPLES
1016
	 *
1017
	 * wp jetpack sync_queue full_sync peek
1018
	 *
1019
	 * @synopsis <incremental|full_sync> <peek>
1020
	 */
1021
	public function sync_queue( $args, $assoc_args ) {
1022
		if ( ! Actions::sync_allowed() ) {
1023
			WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site.', 'jetpack' ) );
1024
		}
1025
1026
		$queue_name = isset( $args[0] ) ? $args[0] : 'sync';
1027
		$action     = isset( $args[1] ) ? $args[1] : 'peek';
1028
1029
		// We map the queue name that way we can support more friendly queue names in the commands, but still use
1030
		// the queue name that the code expects.
1031
		$queue_name_map    = $allowed_queues = array(
1032
			'incremental' => 'sync',
1033
			'full'        => 'full_sync',
1034
		);
1035
		$mapped_queue_name = isset( $queue_name_map[ $queue_name ] ) ? $queue_name_map[ $queue_name ] : $queue_name;
1036
1037
		switch ( $action ) {
1038
			case 'peek':
1039
				$queue = new Queue( $mapped_queue_name );
1040
				$items = $queue->peek( 100 );
1041
1042
				if ( empty( $items ) ) {
1043
					/* translators: %s is the name of the queue, either 'incremental' or 'full' */
1044
					WP_CLI::log( sprintf( __( 'Nothing is in the queue: %s', 'jetpack' ), $queue_name ) );
1045
				} else {
1046
					$collection = array();
1047
					foreach ( $items as $item ) {
1048
						$collection[] = array(
1049
							'action'          => $item[0],
1050
							'args'            => json_encode( $item[1] ),
1051
							'current_user_id' => $item[2],
1052
							'microtime'       => $item[3],
1053
							'importing'       => (string) $item[4],
1054
						);
1055
					}
1056
					WP_CLI\Utils\format_items(
1057
						'table',
1058
						$collection,
1059
						array(
1060
							'action',
1061
							'args',
1062
							'current_user_id',
1063
							'microtime',
1064
							'importing',
1065
						)
1066
					);
1067
				}
1068
				break;
1069
		}
1070
	}
1071
1072
	/**
1073
	 * Cancel's the current Jetpack plan granted by this partner, if applicable
1074
	 *
1075
	 * Returns success or error JSON
1076
	 *
1077
	 * <token_json>
1078
	 * : JSON blob of WPCOM API token
1079
	 *  [--partner_tracking_id=<partner_tracking_id>]
1080
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1081
	 *
1082
	 *  * @synopsis <token_json> [--partner_tracking_id=<partner_tracking_id>]
1083
	 */
1084
	public function partner_cancel( $args, $named_args ) {
1085
		list( $token_json ) = $args;
1086
1087 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1088
			/* translators: %s is the invalid JSON string */
1089
			$this->partner_provision_error( new WP_Error( 'missing_access_token', sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
1090
		}
1091
1092
		if ( isset( $token->error ) ) {
1093
			$this->partner_provision_error( new WP_Error( $token->error, $token->message ) );
1094
		}
1095
1096
		if ( ! isset( $token->access_token ) ) {
1097
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
1098
		}
1099
1100
		if ( Jetpack::validate_sync_error_idc_option() ) {
1101
			$this->partner_provision_error(
1102
				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
1109
		$site_identifier = Jetpack_Options::get_option( 'id' );
1110
1111
		if ( ! $site_identifier ) {
1112
			$site_identifier = Jetpack::build_raw_urls( get_home_url() );
1113
		}
1114
1115
		$request = array(
1116
			'headers' => array(
1117
				'Authorization' => 'Bearer ' . $token->access_token,
1118
				'Host'          => 'public-api.wordpress.com',
1119
			),
1120
			'timeout' => 60,
1121
			'method'  => 'POST',
1122
		);
1123
1124
		$url = sprintf( 'https://%s/rest/v1.3/jpphp/%s/partner-cancel', $this->get_api_host(), $site_identifier );
1125 View Code Duplication
		if ( ! empty( $named_args ) && ! empty( $named_args['partner_tracking_id'] ) ) {
1126
			$url = esc_url_raw( add_query_arg( 'partner_tracking_id', $named_args['partner_tracking_id'], $url ) );
1127
		}
1128
1129
		$result = Client::_wp_remote_request( $url, $request );
1130
1131
		Jetpack_Options::delete_option( 'onboarding' );
1132
1133
		if ( is_wp_error( $result ) ) {
1134
			$this->partner_provision_error( $result );
1135
		}
1136
1137
		WP_CLI::log( wp_remote_retrieve_body( $result ) );
1138
	}
1139
1140
	/**
1141
	 * Provision a site using a Jetpack Partner license
1142
	 *
1143
	 * Returns JSON blob
1144
	 *
1145
	 * ## OPTIONS
1146
	 *
1147
	 * <token_json>
1148
	 * : JSON blob of WPCOM API token
1149
	 * [--plan=<plan_name>]
1150
	 * : Slug of the requested plan, e.g. premium
1151
	 * [--wpcom_user_id=<user_id>]
1152
	 * : WordPress.com ID of user to connect as (must be whitelisted against partner key)
1153
	 * [--wpcom_user_email=<wpcom_user_email>]
1154
	 * : Override the email we send to WordPress.com for registration
1155
	 * [--onboarding=<onboarding>]
1156
	 * : Guide the user through an onboarding wizard
1157
	 * [--force_register=<register>]
1158
	 * : Whether to force a site to register
1159
	 * [--force_connect=<force_connect>]
1160
	 * : Force JPS to not reuse existing credentials
1161
	 * [--home_url=<home_url>]
1162
	 * : Overrides the home option via the home_url filter, or the WP_HOME constant
1163
	 * [--site_url=<site_url>]
1164
	 * : Overrides the siteurl option via the site_url filter, or the WP_SITEURL constant
1165
	 * [--partner_tracking_id=<partner_tracking_id>]
1166
	 * : This is an optional ID that a host can pass to help identify a site in logs on WordPress.com
1167
	 *
1168
	 * ## EXAMPLES
1169
	 *
1170
	 *     $ wp jetpack partner_provision '{ some: "json" }' premium 1
1171
	 *     { success: true }
1172
	 *
1173
	 * @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>]
1174
	 */
1175
	public function partner_provision( $args, $named_args ) {
1176
		list( $token_json ) = $args;
1177
1178 View Code Duplication
		if ( ! $token_json || ! ( $token = json_decode( $token_json ) ) ) {
1179
			/* translators: %s is the invalid JSON string */
1180
			$this->partner_provision_error( new WP_Error( 'missing_access_token', sprintf( __( 'Invalid token JSON: %s', 'jetpack' ), $token_json ) ) );
1181
		}
1182
1183
		if ( isset( $token->error ) ) {
1184
			$message = isset( $token->message )
1185
				? $token->message
1186
				: '';
1187
			$this->partner_provision_error( new WP_Error( $token->error, $message ) );
1188
		}
1189
1190
		if ( ! isset( $token->access_token ) ) {
1191
			$this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
1192
		}
1193
1194
		require_once JETPACK__PLUGIN_DIR . '_inc/class.jetpack-provision.php';
1195
1196
		$body_json = Jetpack_Provision::partner_provision( $token->access_token, $named_args );
1197
1198
		if ( is_wp_error( $body_json ) ) {
1199
			error_log(
1200
				json_encode(
1201
					array(
1202
						'success'       => false,
1203
						'error_code'    => $body_json->get_error_code(),
1204
						'error_message' => $body_json->get_error_message(),
1205
					)
1206
				)
1207
			);
1208
			exit( 1 );
1209
		}
1210
1211
		WP_CLI::log( json_encode( $body_json ) );
1212
	}
1213
1214
	/**
1215
	 * Manages your Jetpack sitemap
1216
	 *
1217
	 * ## OPTIONS
1218
	 *
1219
	 * rebuild : Rebuild all sitemaps
1220
	 * --purge : if set, will remove all existing sitemap data before rebuilding
1221
	 *
1222
	 * ## EXAMPLES
1223
	 *
1224
	 * wp jetpack sitemap rebuild
1225
	 *
1226
	 * @subcommand sitemap
1227
	 * @synopsis <rebuild> [--purge]
1228
	 */
1229
	public function sitemap( $args, $assoc_args ) {
1230
		if ( ! Jetpack::is_active() ) {
1231
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1232
		}
1233
		if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1234
			WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1235
		}
1236
		if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1237
			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' ) );
1238
		}
1239
1240
		if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1241
			$librarian = new Jetpack_Sitemap_Librarian();
1242
			$librarian->delete_all_stored_sitemap_data();
1243
		}
1244
1245
		$sitemap_builder = new Jetpack_Sitemap_Builder();
1246
		$sitemap_builder->update_sitemap();
1247
	}
1248
1249
	/**
1250
	 * Allows authorizing a user via the command line and will activate
1251
	 *
1252
	 * ## EXAMPLES
1253
	 *
1254
	 * wp jetpack authorize_user --token=123456789abcdef
1255
	 *
1256
	 * @synopsis --token=<value>
1257
	 */
1258
	public function authorize_user( $args, $named_args ) {
1259
		if ( ! is_user_logged_in() ) {
1260
			WP_CLI::error( __( 'Please select a user to authorize via the --user global argument.', 'jetpack' ) );
1261
		}
1262
1263
		if ( empty( $named_args['token'] ) ) {
1264
			WP_CLI::error( __( 'A non-empty token argument must be passed.', 'jetpack' ) );
1265
		}
1266
1267
		$is_master_user  = ! Jetpack::is_active();
1268
		$current_user_id = get_current_user_id();
1269
1270
		Jetpack::update_user_token( $current_user_id, sprintf( '%s.%d', $named_args['token'], $current_user_id ), $is_master_user );
1271
1272
		WP_CLI::log( wp_json_encode( $named_args ) );
1273
1274
		if ( $is_master_user ) {
1275
			/**
1276
			 * Auto-enable SSO module for new Jetpack Start connections
1277
			*
1278
			* @since 5.0.0
1279
			*
1280
			* @param bool $enable_sso Whether to enable the SSO module. Default to true.
1281
			*/
1282
			$enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
1283
			Jetpack::handle_post_authorization_actions( $enable_sso, false );
1284
1285
			/* translators: %d is a user ID */
1286
			WP_CLI::success( sprintf( __( 'Authorized %d and activated default modules.', 'jetpack' ), $current_user_id ) );
1287
		} else {
1288
			/* translators: %d is a user ID */
1289
			WP_CLI::success( sprintf( __( 'Authorized %d.', 'jetpack' ), $current_user_id ) );
1290
		}
1291
	}
1292
1293
	/**
1294
	 * Allows calling a WordPress.com API endpoint using the current blog's token.
1295
	 *
1296
	 * ## OPTIONS
1297
	 * --resource=<resource>
1298
	 * : The resource to call with the current blog's token, where `%d` represents the current blog's ID.
1299
	 *
1300
	 * [--api_version=<api_version>]
1301
	 * : The API version to query against.
1302
	 *
1303
	 * [--base_api_path=<base_api_path>]
1304
	 * : The base API path to query.
1305
	 * ---
1306
	 * default: rest
1307
	 * ---
1308
	 *
1309
	 * [--body=<body>]
1310
	 * : A JSON encoded string representing arguments to send in the body.
1311
	 *
1312
	 * [--field=<value>]
1313
	 * : Any number of arguments that should be passed to the resource.
1314
	 *
1315
	 * [--pretty]
1316
	 * : Will pretty print the results of a successful API call.
1317
	 *
1318
	 * [--strip-success]
1319
	 * : Will remove the green success label from successful API calls.
1320
	 *
1321
	 * ## EXAMPLES
1322
	 *
1323
	 * wp jetpack call_api --resource='/sites/%d'
1324
	 */
1325
	public function call_api( $args, $named_args ) {
1326
		if ( ! Jetpack::is_active() ) {
1327
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1328
		}
1329
1330
		$consumed_args = array(
1331
			'resource',
1332
			'api_version',
1333
			'base_api_path',
1334
			'body',
1335
			'pretty',
1336
		);
1337
1338
		// Get args that should be passed to resource.
1339
		$other_args = array_diff_key( $named_args, array_flip( $consumed_args ) );
1340
1341
		$decoded_body = ! empty( $named_args['body'] )
1342
			? json_decode( $named_args['body'], true )
1343
			: false;
1344
1345
		$resource_url = ( false === strpos( $named_args['resource'], '%d' ) )
1346
			? $named_args['resource']
1347
			: sprintf( $named_args['resource'], Jetpack_Options::get_option( 'id' ) );
1348
1349
		$response = Client::wpcom_json_api_request_as_blog(
1350
			$resource_url,
1351
			empty( $named_args['api_version'] ) ? Client::WPCOM_JSON_API_VERSION : $named_args['api_version'],
1352
			$other_args,
1353
			empty( $decoded_body ) ? null : $decoded_body,
1354
			empty( $named_args['base_api_path'] ) ? 'rest' : $named_args['base_api_path']
1355
		);
1356
1357 View Code Duplication
		if ( is_wp_error( $response ) ) {
1358
			WP_CLI::error(
1359
				sprintf(
1360
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an error code, %3$s is an error message. */
1361
					__( 'Request to %1$s returned an error: (%2$d) %3$s.', 'jetpack' ),
1362
					$resource_url,
1363
					$response->get_error_code(),
1364
					$response->get_error_message()
0 ignored issues
show
The method get_error_message() does not seem to exist on object<WP_Error>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1365
				)
1366
			);
1367
		}
1368
1369
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1370
			WP_CLI::error(
1371
				sprintf(
1372
					/* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an HTTP status code. */
1373
					__( 'Request to %1$s returned a non-200 response code: %2$d.', 'jetpack' ),
1374
					$resource_url,
1375
					wp_remote_retrieve_response_code( $response )
1376
				)
1377
			);
1378
		}
1379
1380
		$output = wp_remote_retrieve_body( $response );
1381
		if ( isset( $named_args['pretty'] ) ) {
1382
			$decoded_output = json_decode( $output );
1383
			if ( $decoded_output ) {
1384
				$output = wp_json_encode( $decoded_output, JSON_PRETTY_PRINT );
1385
			}
1386
		}
1387
1388
		if ( isset( $named_args['strip-success'] ) ) {
1389
			WP_CLI::log( $output );
1390
			WP_CLI::halt( 0 );
1391
		}
1392
1393
		WP_CLI::success( $output );
1394
	}
1395
1396
	/**
1397
	 * Allows uploading SSH Credentials to the current site for backups, restores, and security scanning.
1398
	 *
1399
	 * ## OPTIONS
1400
	 *
1401
	 * [--host=<host>]
1402
	 * : The SSH server's address.
1403
	 *
1404
	 * [--ssh-user=<user>]
1405
	 * : The username to use to log in to the SSH server.
1406
	 *
1407
	 * [--pass=<pass>]
1408
	 * : The password used to log in, if using a password. (optional)
1409
	 *
1410
	 * [--kpri=<kpri>]
1411
	 * : The private key used to log in, if using a private key. (optional)
1412
	 *
1413
	 * [--pretty]
1414
	 * : Will pretty print the results of a successful API call. (optional)
1415
	 *
1416
	 * [--strip-success]
1417
	 * : Will remove the green success label from successful API calls. (optional)
1418
	 *
1419
	 * ## EXAMPLES
1420
	 *
1421
	 * wp jetpack upload_ssh_creds --host=example.com --ssh-user=example --pass=password
1422
	 * wp jetpack updload_ssh_creds --host=example.com --ssh-user=example --kpri=key
1423
	 */
1424
	public function upload_ssh_creds( $args, $named_args ) {
1425
		if ( ! Jetpack::is_active() ) {
1426
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1427
		}
1428
1429
		$required_args = array(
1430
			'host',
1431
			'ssh-user',
1432
		);
1433
1434
		foreach ( $required_args as $arg ) {
1435
			if ( empty( $named_args[ $arg ] ) ) {
1436
				WP_CLI::error(
1437
					sprintf(
1438
						/* translators: %s is a slug, such as 'host'. */
1439
						__( '`%s` cannot be empty.', 'jetpack' ),
1440
						$arg
1441
					)
1442
				);
1443
			}
1444
		}
1445
1446
		if ( empty( $named_args['pass'] ) && empty( $named_args['kpri'] ) ) {
1447
			WP_CLI::error( __( 'Both `pass` and `kpri` fields cannot be blank.', 'jetpack' ) );
1448
		}
1449
1450
		$values = array(
1451
			'credentials' => array(
1452
				'site_url' => get_site_url(),
1453
				'abspath'  => ABSPATH,
1454
				'protocol' => 'ssh',
1455
				'port'     => 22,
1456
				'role'     => 'main',
1457
				'host'     => $named_args['host'],
1458
				'user'     => $named_args['ssh-user'],
1459
				'pass'     => empty( $named_args['pass'] ) ? '' : $named_args['pass'],
1460
				'kpri'     => empty( $named_args['kpri'] ) ? '' : $named_args['kpri'],
1461
			),
1462
		);
1463
1464
		$named_args = wp_parse_args(
1465
			array(
1466
				'resource'    => '/activity-log/%d/update-credentials',
1467
				'method'      => 'POST',
1468
				'api_version' => '1.1',
1469
				'body'        => wp_json_encode( $values ),
1470
				'timeout'     => 30,
1471
			),
1472
			$named_args
1473
		);
1474
1475
		self::call_api( $args, $named_args );
1476
	}
1477
1478
	/**
1479
	 * API wrapper for getting stats from the WordPress.com API for the current site.
1480
	 *
1481
	 * ## OPTIONS
1482
	 *
1483
	 * [--quantity=<quantity>]
1484
	 * : The number of units to include.
1485
	 * ---
1486
	 * default: 30
1487
	 * ---
1488
	 *
1489
	 * [--period=<period>]
1490
	 * : The unit of time to query stats for.
1491
	 * ---
1492
	 * default: day
1493
	 * options:
1494
	 *  - day
1495
	 *  - week
1496
	 *  - month
1497
	 *  - year
1498
	 * ---
1499
	 *
1500
	 * [--date=<date>]
1501
	 * : The latest date to return stats for. Ex. - 2018-01-01.
1502
	 *
1503
	 * [--pretty]
1504
	 * : Will pretty print the results of a successful API call.
1505
	 *
1506
	 * [--strip-success]
1507
	 * : Will remove the green success label from successful API calls.
1508
	 *
1509
	 * ## EXAMPLES
1510
	 *
1511
	 * wp jetpack get_stats
1512
	 */
1513
	public function get_stats( $args, $named_args ) {
1514
		$selected_args = array_intersect_key(
1515
			$named_args,
1516
			array_flip(
1517
				array(
1518
					'quantity',
1519
					'date',
1520
				)
1521
			)
1522
		);
1523
1524
		// The API expects unit, but period seems to be more correct.
1525
		$selected_args['unit'] = $named_args['period'];
1526
1527
		$command = sprintf(
1528
			'jetpack call_api --resource=/sites/%d/stats/%s',
1529
			Jetpack_Options::get_option( 'id' ),
1530
			add_query_arg( $selected_args, 'visits' )
1531
		);
1532
1533
		if ( isset( $named_args['pretty'] ) ) {
1534
			$command .= ' --pretty';
1535
		}
1536
1537
		if ( isset( $named_args['strip-success'] ) ) {
1538
			$command .= ' --strip-success';
1539
		}
1540
1541
		WP_CLI::runcommand(
1542
			$command,
1543
			array(
1544
				'launch' => false, // Use the current process.
1545
			)
1546
		);
1547
	}
1548
1549
	/**
1550
	 * Allows management of publicize connections.
1551
	 *
1552
	 * ## OPTIONS
1553
	 *
1554
	 * <list|disconnect>
1555
	 * : The action to perform.
1556
	 * ---
1557
	 * options:
1558
	 *   - list
1559
	 *   - disconnect
1560
	 * ---
1561
	 *
1562
	 * [<identifier>]
1563
	 * : The connection ID or service to perform an action on.
1564
	 *
1565
	 * [--format=<format>]
1566
	 * : Allows overriding the output of the command when listing connections.
1567
	 * ---
1568
	 * default: table
1569
	 * options:
1570
	 *   - table
1571
	 *   - json
1572
	 *   - csv
1573
	 *   - yaml
1574
	 *   - ids
1575
	 *   - count
1576
	 * ---
1577
	 *
1578
	 * ## EXAMPLES
1579
	 *
1580
	 *     # List all publicize connections.
1581
	 *     $ wp jetpack publicize list
1582
	 *
1583
	 *     # List publicize connections for a given service.
1584
	 *     $ wp jetpack publicize list twitter
1585
	 *
1586
	 *     # List all publicize connections for a given user.
1587
	 *     $ wp --user=1 jetpack publicize list
1588
	 *
1589
	 *     # List all publicize connections for a given user and service.
1590
	 *     $ wp --user=1 jetpack publicize list twitter
1591
	 *
1592
	 *     # Display details for a given connection.
1593
	 *     $ wp jetpack publicize list 123456
1594
	 *
1595
	 *     # Diconnection a given connection.
1596
	 *     $ wp jetpack publicize disconnect 123456
1597
	 *
1598
	 *     # Disconnect all connections.
1599
	 *     $ wp jetpack publicize disconnect all
1600
	 *
1601
	 *     # Disconnect all connections for a given service.
1602
	 *     $ wp jetpack publicize disconnect twitter
1603
	 */
1604
	public function publicize( $args, $named_args ) {
1605
		if ( ! Jetpack::is_active() ) {
1606
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1607
		}
1608
1609
		if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1610
			WP_CLI::error( __( 'The publicize module is not active.', 'jetpack' ) );
1611
		}
1612
1613
		if ( Jetpack::is_development_mode() ) {
1614
			if (
1615
				! defined( 'JETPACK_DEV_DEBUG' ) &&
1616
				! has_filter( 'jetpack_development_mode' ) &&
1617
				false === strpos( site_url(), '.' )
1618
			) {
1619
				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' ) );
1620
			}
1621
1622
			WP_CLI::error( __( 'Jetpack is currently in development mode, so the publicize module will not load.', 'jetpack' ) );
1623
		}
1624
1625
		if ( ! class_exists( 'Publicize' ) ) {
1626
			WP_CLI::error( __( 'The publicize module is not loaded.', 'jetpack' ) );
1627
		}
1628
1629
		$action        = $args[0];
1630
		$publicize     = new Publicize();
1631
		$identifier    = ! empty( $args[1] ) ? $args[1] : false;
1632
		$services      = array_keys( $publicize->get_services() );
1633
		$id_is_service = in_array( $identifier, $services, true );
1634
1635
		switch ( $action ) {
1636
			case 'list':
1637
				$connections_to_return = array();
1638
1639
				// For the CLI command, let's return all connections when a user isn't specified. This
1640
				// differs from the logic in the Publicize class.
1641
				$option_connections = is_user_logged_in()
1642
					? (array) $publicize->get_all_connections_for_user()
1643
					: (array) $publicize->get_all_connections();
1644
1645
				foreach ( $option_connections as $service_name => $connections ) {
1646
					foreach ( (array) $connections as $id => $connection ) {
1647
						$connection['id']        = $id;
1648
						$connection['service']   = $service_name;
1649
						$connections_to_return[] = $connection;
1650
					}
1651
				}
1652
1653
				if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1654
					$temp_connections      = $connections_to_return;
1655
					$connections_to_return = array();
1656
1657
					foreach ( $temp_connections as $connection ) {
1658
						if ( $identifier === $connection['service'] ) {
1659
							$connections_to_return[] = $connection;
1660
						}
1661
					}
1662
				}
1663
1664
				if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1665
					$connections_to_return = wp_list_filter( $connections_to_return, array( 'id' => $identifier ) );
1666
				}
1667
1668
				$expected_keys = array(
1669
					'id',
1670
					'service',
1671
					'user_id',
1672
					'provider',
1673
					'issued',
1674
					'expires',
1675
					'external_id',
1676
					'external_name',
1677
					'external_display',
1678
					'type',
1679
					'connection_data',
1680
				);
1681
1682
				// Somehow, a test site ended up in a state where $connections_to_return looked like:
1683
				// array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1684
				// This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1685
				// to minimize future issues, this nested loop will remove any connections that don't contain
1686
				// any keys that we expect.
1687
				foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1688
					foreach ( $expected_keys as $expected_key ) {
1689
						if ( ! isset( $connection[ $expected_key ] ) ) {
1690
							unset( $connections_to_return[ $connection_key ] );
1691
							continue;
1692
						}
1693
					}
1694
				}
1695
1696
				if ( empty( $connections_to_return ) ) {
1697
					return false;
1698
				}
1699
1700
				WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1701
				break; // list.
1702
			case 'disconnect':
1703
				if ( ! $identifier ) {
1704
					WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1705
				}
1706
1707
				// If the connection ID is 'all' then delete all connections. If the connection ID
1708
				// matches a service, delete all connections for that service.
1709
				if ( 'all' === $identifier || $id_is_service ) {
1710
					if ( 'all' === $identifier ) {
1711
						WP_CLI::log( __( "You're about to delete all publicize connections.", 'jetpack' ) );
1712
					} else {
1713
						/* translators: %s is a lowercase string for a social network. */
1714
						WP_CLI::log( sprintf( __( "You're about to delete all publicize connections to %s.", 'jetpack' ), $identifier ) );
1715
					}
1716
1717
					jetpack_cli_are_you_sure();
1718
1719
					$connections = array();
1720
					$service     = $identifier;
1721
1722
					$option_connections = is_user_logged_in()
1723
						? (array) $publicize->get_all_connections_for_user()
1724
						: (array) $publicize->get_all_connections();
1725
1726
					if ( 'all' === $service ) {
1727
						foreach ( (array) $option_connections as $service_name => $service_connections ) {
1728
							foreach ( $service_connections as $id => $connection ) {
1729
								$connections[ $id ] = $connection;
1730
							}
1731
						}
1732
					} elseif ( ! empty( $option_connections[ $service ] ) ) {
1733
						$connections = $option_connections[ $service ];
1734
					}
1735
1736
					if ( ! empty( $connections ) ) {
1737
						$count    = count( $connections );
1738
						$progress = \WP_CLI\Utils\make_progress_bar(
1739
							/* translators: %s is a lowercase string for a social network. */
1740
							sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1741
							$count
1742
						);
1743
1744
						foreach ( $connections as $id => $connection ) {
1745
							if ( false === $publicize->disconnect( false, $id ) ) {
1746
								WP_CLI::error(
1747
									sprintf(
1748
										/* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1749
										__( 'Publicize connection %d could not be disconnected', 'jetpack' ),
1750
										$id
1751
									)
1752
								);
1753
							}
1754
1755
							$progress->tick();
1756
						}
1757
1758
						$progress->finish();
1759
1760
						if ( 'all' === $service ) {
1761
							WP_CLI::success( __( 'All publicize connections were successfully disconnected.', 'jetpack' ) );
1762
						} else {
1763
							/* translators: %s is a lowercase string for a social network. */
1764
							WP_CLI::success( __( 'All publicize connections to %s were successfully disconnected.', 'jetpack' ), $service );
1765
						}
1766
					}
1767
				} else {
1768
					if ( false !== $publicize->disconnect( false, $identifier ) ) {
1769
						/* translators: %d is a numeric ID. Example: 1234. */
1770
						WP_CLI::success( sprintf( __( 'Publicize connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1771
					} else {
1772
						/* translators: %d is a numeric ID. Example: 1234. */
1773
						WP_CLI::error( sprintf( __( 'Publicize connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1774
					}
1775
				}
1776
				break; // disconnect.
1777
		}
1778
	}
1779
1780
	private function get_api_host() {
1781
		$env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1782
		return $env_api_host ? $env_api_host : JETPACK__WPCOM_JSON_API_HOST;
1783
	}
1784
1785
	private function partner_provision_error( $error ) {
1786
		WP_CLI::log(
1787
			json_encode(
1788
				array(
1789
					'success'       => false,
1790
					'error_code'    => $error->get_error_code(),
1791
					'error_message' => $error->get_error_message(),
1792
				)
1793
			)
1794
		);
1795
		exit( 1 );
1796
	}
1797
1798
	/**
1799
	 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1800
	 *
1801
	 * ## TYPES
1802
	 *
1803
	 * 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.
1804
	 *
1805
	 * ## BLOCK TYPE OPTIONS
1806
	 *
1807
	 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1808
	 * 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.
1809
	 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1810
	 * --description: Allows to provide a text description of the block.
1811
	 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1812
	 *
1813
	 * ## BLOCK TYPE EXAMPLES
1814
	 *
1815
	 * wp jetpack scaffold block "Cool Block"
1816
	 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1817
	 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1818
	 *
1819
	 * @subcommand scaffold block
1820
	 * @synopsis <type> <title> [--slug] [--description] [--keywords]
1821
	 *
1822
	 * @param array $args       Positional parameters, when strings are passed, wrap them in quotes.
1823
	 * @param array $assoc_args Associative parameters like --slug="nice-block".
1824
	 */
1825
	public function scaffold( $args, $assoc_args ) {
1826
		// It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1827
		switch ( $args[0] ) {
1828
			case 'block':
1829
				$this->block( $args, $assoc_args );
1830
				break;
1831
			default:
1832
				/* translators: %s is the subcommand */
1833
				WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1834
				exit( 1 );
1835
		}
1836
	}
1837
1838
	/**
1839
	 * Creates the essential files in Jetpack to build a Gutenberg block.
1840
	 *
1841
	 * @param array $args       Positional parameters. Only one is used, that corresponds to the block title.
1842
	 * @param array $assoc_args Associative parameters defined in the scaffold() method.
1843
	 */
1844
	public function block( $args, $assoc_args ) {
1845 View Code Duplication
		if ( isset( $args[1] ) ) {
1846
			$title = ucwords( $args[1] );
1847
		} else {
1848
			WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
1849
			exit( 1 );
1850
		}
1851
1852
		$slug = isset( $assoc_args['slug'] )
1853
			? $assoc_args['slug']
1854
			: sanitize_title( $title );
1855
1856
		if ( preg_match( '#^jetpack/#', $slug ) ) {
1857
			$slug = preg_replace( '#^jetpack/#', '', $slug );
1858
		}
1859
1860
		if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
1861
			WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
1862
		}
1863
1864
		global $wp_filesystem;
1865
		if ( ! WP_Filesystem() ) {
1866
			WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
1867
		}
1868
1869
		$path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
1870
1871
		if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
1872
			/* translators: %s is path to the conflicting block */
1873
			WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
1874
			exit( 1 );
1875
		}
1876
1877
		$wp_filesystem->mkdir( $path );
1878
1879
		$hasKeywords = isset( $assoc_args['keywords'] );
1880
1881
		$files = array(
1882
			"$path/$slug.php"   => $this->render_block_file(
1883
				'block-register-php',
1884
				array(
1885
					'slug'            => $slug,
1886
					'title'           => $title,
1887
					'underscoredSlug' => str_replace( '-', '_', $slug ),
1888
				)
1889
			),
1890
			"$path/index.js"    => $this->render_block_file(
1891
				'block-index-js',
1892
				array(
1893
					'slug'        => $slug,
1894
					'title'       => $title,
1895
					'description' => isset( $assoc_args['description'] )
1896
						? $assoc_args['description']
1897
						: $title,
1898
					'keywords'    => $hasKeywords
1899
					? array_map(
1900
						function( $keyword ) {
1901
								// Construction necessary for Mustache lists
1902
								return array( 'keyword' => trim( $keyword ) );
1903
						},
1904
						explode( ',', $assoc_args['keywords'], 3 )
1905
					)
1906
					: '',
1907
					'hasKeywords' => $hasKeywords,
1908
				)
1909
			),
1910
			"$path/editor.js"   => $this->render_block_file( 'block-editor-js' ),
1911
			"$path/editor.scss" => $this->render_block_file(
1912
				'block-editor-scss',
1913
				array(
1914
					'slug'  => $slug,
1915
					'title' => $title,
1916
				)
1917
			),
1918
			"$path/edit.js"     => $this->render_block_file(
1919
				'block-edit-js',
1920
				array(
1921
					'title'     => $title,
1922
					'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
1923
				)
1924
			),
1925
		);
1926
1927
		$files_written = array();
1928
1929
		foreach ( $files as $filename => $contents ) {
1930
			if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
1931
				$files_written[] = $filename;
1932
			} else {
1933
				/* translators: %s is a file name */
1934
				WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
1935
			}
1936
		}
1937
1938
		if ( empty( $files_written ) ) {
1939
			WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
1940
		} else {
1941
			// Load index.json and insert the slug of the new block in the production array
1942
			$block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
1943
			$block_list      = $wp_filesystem->get_contents( $block_list_path );
1944
			if ( empty( $block_list ) ) {
1945
				/* translators: %s is the path to the file with the block list */
1946
				WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
1947
			} elseif ( false === stripos( $block_list, $slug ) ) {
1948
				$new_block_list         = json_decode( $block_list );
1949
				$new_block_list->beta[] = $slug;
1950
				if ( ! $wp_filesystem->put_contents( $block_list_path, wp_json_encode( $new_block_list ) ) ) {
1951
					/* translators: %s is the path to the file with the block list */
1952
					WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
1953
				}
1954
			}
1955
1956
			WP_CLI::success(
1957
				sprintf(
1958
					/* translators: the placeholders are a human readable title, and a series of words separated by dashes */
1959
					esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
1960
					"--------------------------------------------------------------------------------------------------------------------\n" .
1961
					/* translators: the placeholder is a directory path */
1962
					esc_html__( 'The files were created at %s', 'jetpack' ) . "\n" .
1963
					esc_html__( 'To start using the block, build the blocks with yarn run build-extensions', 'jetpack' ) . "\n" .
1964
					/* translators: the placeholder is a file path */
1965
					esc_html__( 'The block slug has been added to the beta list at %s', 'jetpack' ) . "\n" .
1966
					esc_html__( 'To load the block, add the constant JETPACK_BETA_BLOCKS as true to your wp-config.php file', 'jetpack' ) . "\n" .
1967
					/* translators: the placeholder is a URL */
1968
					"\n" . esc_html__( 'Read more at %s', 'jetpack' ) . "\n",
1969
					$title,
1970
					$slug,
1971
					$path,
1972
					$block_list_path,
1973
					'https://github.com/Automattic/jetpack/blob/master/extensions/README.md#develop-new-blocks'
1974
				) . '--------------------------------------------------------------------------------------------------------------------'
1975
			);
1976
		}
1977
	}
1978
1979
	/**
1980
	 * Built the file replacing the placeholders in the template with the data supplied.
1981
	 *
1982
	 * @param string $template
1983
	 * @param array  $data
1984
	 *
1985
	 * @return string mixed
1986
	 */
1987
	private static function render_block_file( $template, $data = array() ) {
1988
		return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
1989
	}
1990
}
1991
1992
/*
1993
 * Standard "ask for permission to continue" function.
1994
 * If action cancelled, ask if they need help.
1995
 *
1996
 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
1997
 *
1998
 * @param $flagged   bool   false = normal option | true = flagged by get_jetpack_options_for_reset()
1999
 * @param $error_msg string (optional)
2000
 */
2001
function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
2002
	$cli = new Jetpack_CLI();
2003
2004
	// Default cancellation message
2005
	if ( ! $error_msg ) {
2006
		$error_msg =
2007
			__( 'Action cancelled. Have a question?', 'jetpack' )
2008
			. ' '
2009
			. $cli->green_open
2010
			. 'jetpack.com/support'
2011
			. $cli->color_close;
2012
	}
2013
2014
	if ( ! $flagged ) {
2015
		$prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
2016
	} else {
2017
		$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' );
2018
	}
2019
2020
	WP_CLI::line( $prompt_message );
2021
	$handle = fopen( 'php://stdin', 'r' );
2022
	$line   = fgets( $handle );
2023
	if ( 'yes' != trim( $line ) ) {
2024
		WP_CLI::error( $error_msg );
2025
	}
2026
}
2027