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