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