Completed
Push — add/user-authentication ( ea9fdf...93b532 )
by
unknown
08:44
created

class.jetpack-cli.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
WP_CLI::add_command( 'jetpack', 'Jetpack_CLI' );
4
5
use Automattic\Jetpack\Connection\Client;
6
use Automattic\Jetpack\Sync\Listener;
7
8
/**
9
 * Control your local Jetpack installation.
10
 *
11
 * Minimum PHP requirement for WP-CLI is PHP 5.3, so ignore PHP 5.2 compatibility issues.
12
 * @phpcs:disable PHPCompatibility.PHP.NewLanguageConstructs.t_ns_separatorFound
13
 */
14
class Jetpack_CLI extends WP_CLI_Command {
15
	// Aesthetics
16
	public $green_open  = "\033[32m";
17
	public $red_open    = "\033[31m";
18
	public $yellow_open = "\033[33m";
19
	public $color_close = "\033[0m";
20
21
	/**
22
	 * Get Jetpack Details
23
	 *
24
	 * ## OPTIONS
25
	 *
26
	 * empty: Leave it empty for basic stats
27
	 *
28
	 * full: View full stats.  It's the data from the heartbeat
29
	 *
30
	 * ## EXAMPLES
31
	 *
32
	 * wp jetpack status
33
	 * wp jetpack status full
34
	 *
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( sprintf(
221
					/* translators: %s is the site URL */
222
					__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
223
					esc_url( get_site_url() )
224
				) );
225
				break;
226
			case 'user':
227
				if ( Jetpack::unlink_user( $user->ID ) ) {
228
					Jetpack::log( 'unlink', $user->ID );
229
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
230
				} else {
231
					/* translators: %s is a username */
232
					WP_CLI::error( sprintf( __( "User %s could not be disconnected. Are you sure they're connected currently?", 'jetpack' ), "{$user->login} <{$user->email}>" ) );
233
				}
234
				break;
235
			case 'prompt':
236
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
237
				break;
238
		}
239
	}
240
241
	/**
242
	 * Reset Jetpack options and settings to default
243
	 *
244
	 * ## OPTIONS
245
	 *
246
	 * modules: Resets modules to default state ( get_default_modules() )
247
	 *
248
	 * options: Resets all Jetpack options except:
249
	 *  - All private options (Blog token, user token, etc...)
250
	 *  - id (The Client ID/WP.com Blog ID of this site)
251
	 *  - master_user
252
	 *  - version
253
	 *  - activated
254
	 *
255
	 * ## EXAMPLES
256
	 *
257
	 * wp jetpack reset options
258
	 * wp jetpack reset modules
259
	 * wp jetpack reset sync-checksum --dry-run --offset=0
260
	 *
261
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
262
	 *
263
	 */
264
	public function reset( $args, $assoc_args ) {
265
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
266 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
267
			/* translators: %s is a command like "prompt" */
268
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
269
		}
270
271
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
272
273 View Code Duplication
		if ( $is_dry_run ) {
274
			WP_CLI::warning(
275
				__( "\nThis is a dry run.\n", 'jetpack' ) .
276
				__( "No actions will be taken.\n", 'jetpack' ) .
277
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
278
			);
279
		} else {
280
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
281
			jetpack_cli_are_you_sure();
282
		}
283
284
		switch ( $action ) {
285
			case 'options':
286
				$options_to_reset = Jetpack_Options::get_options_for_reset();
287
				// Reset the Jetpack options
288
				WP_CLI::line( sprintf(
289
					/* translators: %s is the site URL */
290
					__( "Resetting Jetpack Options for %s...\n", "jetpack" ),
291
					esc_url( get_site_url() )
292
				) );
293
				sleep(1); // Take a breath
294 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
295
					if ( ! $is_dry_run ) {
296
						Jetpack_Options::delete_option( $option_to_reset );
297
						usleep( 100000 );
298
					}
299
300
					/* translators: This is the result of an action. The option named %s was reset */
301
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
302
				}
303
304
				// Reset the WP options
305
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", "jetpack" ) );
306
				usleep( 500000 ); // Take a breath
307 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
308
					if ( ! $is_dry_run ) {
309
						delete_option( $option_to_reset );
310
						usleep( 100000 );
311
					}
312
					/* translators: This is the result of an action. The option named %s was reset */
313
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
314
				}
315
316
				// Reset to default modules
317
				WP_CLI::line( __( "Resetting default modules...\n", "jetpack" ) );
318
				usleep( 500000 ); // Take a breath
319
				$default_modules = Jetpack::get_default_modules();
320
				if ( ! $is_dry_run ) {
321
					Jetpack::update_active_modules( $default_modules );
322
				}
323
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
324
325
				// Jumpstart option is special
326
				if ( ! $is_dry_run ) {
327
					Jetpack_Options::update_option( 'jumpstart', 'new_connection' );
328
				}
329
				WP_CLI::success( __( 'jumpstart option reset', 'jetpack' ) );
330
				break;
331 View Code Duplication
			case 'modules':
332
				if ( ! $is_dry_run ) {
333
					$default_modules = Jetpack::get_default_modules();
334
					Jetpack::update_active_modules( $default_modules );
335
				}
336
337
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
338
				break;
339
			case 'prompt':
340
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
341
				break;
342
			case 'sync-checksum':
343
				$option = 'jetpack_callables_sync_checksum';
344
345
				if ( is_multisite() ) {
346
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
347
348
					/*
349
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
350
					 * Offset can be used to paginate and try to clean up more sites.
351
					 */
352
					$sites       = get_sites( array( 'number' => 1000, 'offset' => $offset ) );
353
					$count_fixes = 0;
354
					foreach ( $sites as $site ) {
355
						switch_to_blog( $site->blog_id );
356
						$count = self::count_option( $option );
357
						if ( $count > 1 ) {
358
							if ( ! $is_dry_run ) {
359
								delete_option( $option );
360
							}
361
							WP_CLI::line(
362
								sprintf(
363
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
364
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
365
									$count,
366
									$option,
367
									"{$site->domain}{$site->path}"
368
								)
369
							);
370
							$count_fixes++;
371
							if ( ! $is_dry_run ) {
372
								/*
373
								 * We could be deleting a lot of options rows at the same time.
374
								 * Allow some time for replication to catch up.
375
								 */
376
								sleep( 3 );
377
							}
378
						}
379
380
						restore_current_blog();
381
					}
382
					if ( $count_fixes ) {
383
						WP_CLI::success(
384
							sprintf(
385
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
386
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
387
								$option,
388
								$count_fixes
389
							)
390
						);
391
					} else {
392
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
393
					}
394
					return;
395
				}
396
397
				$count = self::count_option( $option );
398
				if ( $count > 1 ) {
399
					if ( ! $is_dry_run ) {
400
						delete_option( $option );
401
					}
402
					WP_CLI::success(
403
						sprintf(
404
							/* translators: %1$d is a number, %2$s is the name of an option. */
405
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
406
							$count,
407
							$option
408
						)
409
					);
410
					return;
411
				}
412
413
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
414
				break;
415
416
		}
417
	}
418
419
	/**
420
	 * Return the number of times an option appears
421
	 * Normally an option would only appear 1 since the option key is supposed to be unique
422
	 * but if a site hasn't updated the DB schema then that would not be the case.
423
	 *
424
	 * @param string $option Option name.
425
	 *
426
	 * @return int
427
	 */
428
	private static function count_option( $option ) {
429
		global $wpdb;
430
		return (int) $wpdb->get_var(
431
			$wpdb->prepare(
432
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
433
				$option
434
			)
435
		);
436
437
	}
438
439
	/**
440
	 * Manage Jetpack Modules
441
	 *
442
	 * ## OPTIONS
443
	 *
444
	 * <list|activate|deactivate|toggle>
445
	 * : The action to take.
446
	 * ---
447
	 * default: list
448
	 * options:
449
	 *  - list
450
	 *  - activate
451
	 *  - deactivate
452
	 *  - toggle
453
	 * ---
454
	 *
455
	 * [<module_slug>]
456
	 * : The slug of the module to perform an action on.
457
	 *
458
	 * [--format=<format>]
459
	 * : Allows overriding the output of the command when listing modules.
460
	 * ---
461
	 * default: table
462
	 * options:
463
	 *  - table
464
	 *  - json
465
	 *  - csv
466
	 *  - yaml
467
	 *  - ids
468
	 *  - count
469
	 * ---
470
	 *
471
	 * ## EXAMPLES
472
	 *
473
	 * wp jetpack module list
474
	 * wp jetpack module list --format=json
475
	 * wp jetpack module activate stats
476
	 * wp jetpack module deactivate stats
477
	 * wp jetpack module toggle stats
478
	 * wp jetpack module activate all
479
	 * wp jetpack module deactivate all
480
	 */
481
	public function module( $args, $assoc_args ) {
482
		$action = isset( $args[0] ) ? $args[0] : 'list';
483
484
		if ( isset( $args[1] ) ) {
485
			$module_slug = $args[1];
486
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
487
				/* translators: %s is a module slug like "stats" */
488
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
489
			}
490
			if ( 'toggle' === $action ) {
491
				$action = Jetpack::is_module_active( $module_slug )
492
					? 'deactivate'
493
					: 'activate';
494
			}
495
			if ( 'all' === $args[1] ) {
496
				$action = ( 'deactivate' === $action )
497
					? 'deactivate_all'
498
					: 'activate_all';
499
			}
500
		} elseif ( 'list' !== $action ) {
501
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
502
			$action = 'list';
503
		}
504
505
		switch ( $action ) {
506
			case 'list':
507
				$modules_list = array();
508
				$modules      = Jetpack::get_available_modules();
509
				sort( $modules );
510
				foreach ( (array) $modules as $module_slug ) {
511
					if ( 'vaultpress' === $module_slug ) {
512
						continue;
513
					}
514
					$modules_list[] = array(
515
						'slug'   => $module_slug,
516
						'status' => Jetpack::is_module_active( $module_slug )
517
							? __( 'Active', 'jetpack' )
518
							: __( 'Inactive', 'jetpack' ),
519
					);
520
				}
521
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
522
				break;
523
			case 'activate':
524
				$module = Jetpack::get_module( $module_slug );
525
				Jetpack::log( 'activate', $module_slug );
526
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
527
					/* translators: %s is the name of a Jetpack module */
528
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
529
				} else {
530
					/* translators: %s is the name of a Jetpack module */
531
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
532
				}
533
				break;
534 View Code Duplication
			case 'activate_all':
535
				$modules = Jetpack::get_available_modules();
536
				Jetpack::update_active_modules( $modules );
537
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
538
				break;
539
			case 'deactivate':
540
				$module = Jetpack::get_module( $module_slug );
541
				Jetpack::log( 'deactivate', $module_slug );
542
				Jetpack::deactivate_module( $module_slug );
543
				/* translators: %s is the name of a Jetpack module */
544
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
545
				break;
546
			case 'deactivate_all':
547
				Jetpack::delete_active_modules();
548
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
549
				break;
550
			case 'toggle':
551
				// Will never happen, should have been handled above and changed to activate or deactivate.
552
				break;
553
		}
554
	}
555
556
	/**
557
	 * Manage Protect Settings
558
	 *
559
	 * ## OPTIONS
560
	 *
561
	 * whitelist: Whitelist an IP address.  You can also read or clear the whitelist.
562
	 *
563
	 *
564
	 * ## EXAMPLES
565
	 *
566
	 * wp jetpack protect whitelist <ip address>
567
	 * wp jetpack protect whitelist list
568
	 * wp jetpack protect whitelist clear
569
	 *
570
	 * @synopsis <whitelist> [<ip|ip_low-ip_high|list|clear>]
571
	 */
572
	public function protect( $args, $assoc_args ) {
573
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
574
		if ( ! in_array( $action, array( 'whitelist' ) ) ) {
575
			/* translators: %s is a command like "prompt" */
576
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
577
		}
578
		// Check if module is active
579
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
580
			/* translators: %s is a module name */
581
			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__ ) );
582
		}
583
		if ( in_array( $action, array( 'whitelist' ) ) ) {
584
			if ( isset( $args[1] ) ) {
585
				$action = 'whitelist';
586
			} else {
587
				$action = 'prompt';
588
			}
589
		}
590
		switch ( $action ) {
591
			case 'whitelist':
592
				$whitelist         = array();
593
				$new_ip            = $args[1];
594
				$current_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
595
596
				// Build array of IPs that are already whitelisted.
597
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
598
				// low & high range params for jetpack_protect_ip_address_is_in_range();
599
				foreach( $current_whitelist as $whitelisted ) {
600
601
					// IP ranges
602
					if ( $whitelisted->range ) {
603
604
						// Is it already whitelisted?
605
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $whitelisted->range_low, $whitelisted->range_high ) ) {
606
							/* translators: %s is an IP address */
607
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
608
							break;
609
						}
610
						$whitelist[] = $whitelisted->range_low . " - " . $whitelisted->range_high;
611
612
					} else { // Individual IPs
613
614
						// Check if the IP is already whitelisted (single IP only)
615
						if ( $new_ip == $whitelisted->ip_address ) {
616
							/* translators: %s is an IP address */
617
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
618
							break;
619
						}
620
						$whitelist[] = $whitelisted->ip_address;
621
622
					}
623
				}
624
625
				/*
626
				 * List the whitelist
627
				 * Done here because it's easier to read the $whitelist array after it's been rebuilt
628
				 */
629
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
630
					if ( ! empty( $whitelist ) ) {
631
						WP_CLI::success( __( 'Here are your whitelisted IPs:', 'jetpack' ) );
632
						foreach ( $whitelist as $ip ) {
633
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) ) ;
634
						}
635
					} else {
636
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
637
					}
638
					break;
639
				}
640
641
				/*
642
				 * Clear the whitelist
643
				 */
644
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
645
					if ( ! empty( $whitelist ) ) {
646
						$whitelist = array();
647
						jetpack_protect_save_whitelist( $whitelist );
648
						WP_CLI::success( __( 'Cleared all whitelisted IPs', 'jetpack' ) );
649
					} else {
650
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
651
					}
652
					break;
653
				}
654
655
				// Append new IP to whitelist array
656
				array_push( $whitelist, $new_ip );
657
658
				// Save whitelist if there are no errors
659
				$result = jetpack_protect_save_whitelist( $whitelist );
660
				if ( is_wp_error( $result ) ) {
661
					WP_CLI::error( $result );
662
				}
663
664
				/* translators: %s is an IP address */
665
				WP_CLI::success( sprintf( __( '%s has been whitelisted.', 'jetpack' ), $new_ip ) );
666
				break;
667
			case 'prompt':
668
				WP_CLI::error(
669
					__( 'No command found.', 'jetpack' ) . "\n" .
670
					__( 'Please enter the IP address you want to whitelist.', 'jetpack' ) . "\n" .
671
					_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" .
672
					_x( "You can also 'list' or 'clear' the whitelist.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
673
				);
674
				break;
675
		}
676
	}
677
678
	/**
679
	 * Manage Jetpack Options
680
	 *
681
	 * ## OPTIONS
682
	 *
683
	 * list   : List all jetpack options and their values
684
	 * delete : Delete an option
685
	 *          - can only delete options that are white listed.
686
	 * update : update an option
687
	 *          - can only update option strings
688
	 * get    : get the value of an option
689
	 *
690
	 * ## EXAMPLES
691
	 *
692
	 * wp jetpack options list
693
	 * wp jetpack options get    <option_name>
694
	 * wp jetpack options delete <option_name>
695
	 * wp jetpack options update <option_name> [<option_value>]
696
	 *
697
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
698
	 */
699
	public function options( $args, $assoc_args ) {
700
		$action = isset( $args[0] ) ? $args[0] : 'list';
701
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
702
703
		// Jumpstart is special
704
		array_push( $safe_to_modify, 'jumpstart' );
705
706
		// Is the option flagged as unsafe?
707
		$flagged = ! in_array( $args[1], $safe_to_modify );
708
709 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
710
			/* translators: %s is a command like "prompt" */
711
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
712
		}
713
714
		if ( isset( $args[0] ) ) {
715
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
716
				$action = 'get';
717
			} else if ( 'delete' == $args[0] && isset( $args[1] ) ) {
718
				$action = 'delete';
719 View Code Duplication
			} else if ( 'update' == $args[0] && isset( $args[1] ) ) {
720
				$action = 'update';
721
			} else {
722
				$action = 'list';
723
			}
724
		}
725
726
		// Bail if the option isn't found
727
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
728 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
729
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
730
		}
731
732
		// Let's print_r the option if it's an array
733
		// Used in the 'get' and 'list' actions
734
		$option = is_array( $option ) ? print_r( $option ) : $option;
735
736
		switch ( $action ) {
737
			case 'get':
738
				WP_CLI::success( "\t" . $option );
739
				break;
740
			case 'delete':
741
				jetpack_cli_are_you_sure( $flagged );
742
743
				Jetpack_Options::delete_option( $args[1] );
744
				/* translators: %s is the option name */
745
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
746
				break;
747
			case 'update':
748
				jetpack_cli_are_you_sure( $flagged );
749
750
				// Updating arrays would get pretty tricky...
751
				$value = Jetpack_Options::get_option( $args[1] );
752
				if ( $value && is_array( $value ) ) {
753
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
754
				}
755
756
				Jetpack_Options::update_option( $args[1], $args[2] );
757
				/* translators: %1$s is the previous value, %2$s is the new value */
758
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
759
				break;
760
			case 'list':
761
				$options_compact     = Jetpack_Options::get_option_names();
762
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
763
				$options_private     = Jetpack_Options::get_option_names( 'private' );
764
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
765
766
				// Table headers
767
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
768
769
				// List out the options and their values
770
				// Tell them if the value is empty or not
771
				// Tell them if it's an array
772
				foreach ( $options as $option ) {
773
					$value = Jetpack_Options::get_option( $option );
774
					if ( ! $value ) {
775
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
776
						continue;
777
					}
778
779
					if ( ! is_array( $value ) ) {
780
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
781
					} else if ( is_array( $value ) ) {
782
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
783
					}
784
				}
785
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
786
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
787
788
				WP_CLI::success(
789
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
790
					str_pad( 'wp jetpack options get', 26 )    . $option_text . "\n" .
791
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
792
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
793
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
794
				);
795
				break;
796
		}
797
	}
798
799
	/**
800
	 * Get the status of or start a new Jetpack sync.
801
	 *
802
	 * ## OPTIONS
803
	 *
804
	 * status   : Print the current sync status
805
	 * settings : Prints the current sync settings
806
	 * start    : Start a full sync from this site to WordPress.com
807
	 * enable   : Enables sync on the site
808
	 * disable  : Disable sync on a site
809
	 * reset    : Disables sync and Resets the sync queues on a site
810
	 *
811
	 * ## EXAMPLES
812
	 *
813
	 * wp jetpack sync status
814
	 * wp jetpack sync settings
815
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
816
	 * wp jetpack sync enable
817
	 * wp jetpack sync disable
818
	 * wp jetpack sync reset
819
	 * wp jetpack sync reset --queue=full or regular
820
	 *
821
	 * @synopsis <status|start> [--<field>=<value>]
822
	 */
823
	public function sync( $args, $assoc_args ) {
824
825
		$action = isset( $args[0] ) ? $args[0] : 'status';
826
827
		switch ( $action ) {
828
			case 'status':
829
				$status = Jetpack_Sync_Actions::get_sync_status();
830
				$collection = array();
831
				foreach ( $status as $key => $item ) {
832
					$collection[]  = array(
833
						'option' => $key,
834
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
835
					);
836
				}
837
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
838
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
839
				break;
840
			case 'settings':
841
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
842
				foreach( Jetpack_Sync_Settings::get_settings() as $setting => $item ) {
843
					$settings[]  = array(
844
						'setting' => $setting,
845
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
846
					);
847
				}
848
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
849
850
			case 'disable':
851
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
852
				update_option( 'jetpack_sync_settings_disable', 1 );
853
				/* translators: %s is the site URL */
854
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
855
				break;
856
			case 'enable':
857
				Jetpack_Sync_Settings::update_settings( array( 'disable' => 0 ) );
858
				/* translators: %s is the site URL */
859
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
860
				break;
861
			case 'reset':
862
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
863
				update_option( 'jetpack_sync_settings_disable', 1 );
864
865
				/* translators: %s is the site URL */
866
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
867
				$listener = Listener::get_instance();
868
				if ( empty( $assoc_args['queue'] ) ) {
869
					$listener->get_sync_queue()->reset();
870
					$listener->get_full_sync_queue()->reset();
871
					/* translators: %s is the site URL */
872
					WP_CLI::log( sprintf( __( 'Reset Full Sync and Regular Queues Queue on %s', 'jetpack' ), get_site_url() ) );
873
					break;
874
				}
875
876
				if ( ! empty( $assoc_args['queue'] ) ) {
877
					switch ( $assoc_args['queue'] ) {
878 View Code Duplication
						case 'regular':
879
							$listener->get_sync_queue()->reset();
880
							/* translators: %s is the site URL */
881
							WP_CLI::log( sprintf( __( 'Reset Regular Sync Queue on %s', 'jetpack' ), get_site_url() ) );
882
							break;
883 View Code Duplication
						case 'full':
884
							$listener->get_full_sync_queue()->reset();
885
							/* translators: %s is the site URL */
886
							WP_CLI::log( sprintf( __( 'Reset Full Sync Queue on %s', 'jetpack' ), get_site_url() ) );
887
							break;
888
						default:
889
							WP_CLI::error( __( 'Please specify what type of queue do you want to reset: `full` or `regular`.', 'jetpack' ) );
890
							break;
891
					}
892
				}
893
894
				break;
895
			case 'start':
896
				if ( ! Jetpack_Sync_Actions::sync_allowed() ) {
897
					if( ! Jetpack_Sync_Settings::get_setting( 'disable' ) ) {
898
						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' ) );
899
						return;
900
					}
901
					if ( doing_action( 'jetpack_user_authorized' ) || Jetpack::is_active() ) {
902
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. Jetpack is not connected.', 'jetpack' ) );
903
						return;
904
					}
905
					if ( Jetpack::is_development_mode() ) {
906
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in development mode.', 'jetpack' ) );
907
						return;
908
					}
909
					if (  Jetpack::is_staging_site() ) {
910
						WP_CLI::error( __( 'Jetpack sync is not currently allowed for this site. The site is in staging mode.', 'jetpack' ) );
911
						return;
912
					}
913
914
				}
915
				// Get the original settings so that we can restore them later
916
				$original_settings = Jetpack_Sync_Settings::get_settings();
917
918
				// Initialize sync settigns so we can sync as quickly as possible
919
				$sync_settings = wp_parse_args(
920
					array_intersect_key( $assoc_args, Jetpack_Sync_Settings::$valid_settings ),
0 ignored issues
show
The property valid_settings cannot be accessed from this context as it is declared private in class Jetpack_Sync_Settings.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
921
					array(
922
						'sync_wait_time' => 0,
923
						'enqueue_wait_time' => 0,
924
						'queue_max_writes_sec' => 10000,
925
						'max_queue_size_full_sync' => 100000
926
					)
927
				);
928
				Jetpack_Sync_Settings::update_settings( $sync_settings );
929
930
				// Convert comma-delimited string of modules to an array
931 View Code Duplication
				if ( ! empty( $assoc_args['modules'] ) ) {
932
					$modules = array_map( 'trim', explode( ',', $assoc_args['modules'] ) );
933
934
					// Convert the array so that the keys are the module name and the value is true to indicate
935
					// that we want to sync the module
936
					$modules = array_map( '__return_true', array_flip( $modules ) );
937
				}
938
939 View Code Duplication
				foreach ( array( 'posts', 'comments', 'users' ) as $module_name ) {
940
					if (
941
						'users' === $module_name &&
942
						isset( $assoc_args[ $module_name ] ) &&
943
						'initial' === $assoc_args[ $module_name ]
944
					) {
945
						$modules[ 'users' ] = 'initial';
946
					} elseif ( isset( $assoc_args[ $module_name ] ) ) {
947
						$ids = explode( ',', $assoc_args[ $module_name ] );
948
						if ( count( $ids ) > 0 ) {
949
							$modules[ $module_name ] = $ids;
950
						}
951
					}
952
				}
953
954
				if ( empty( $modules ) ) {
955
					$modules = null;
956
				}
957
958
				// Kick off a full sync
959
				if ( Jetpack_Sync_Actions::do_full_sync( $modules ) ) {
960
					if ( $modules ) {
961
						/* translators: %s is a comma separated list of Jetpack modules */
962
						WP_CLI::log( sprintf( __( 'Initialized a new full sync with modules: %s', 'jetpack' ), join( ', ', array_keys( $modules ) ) ) );
963
					} else {
964
						WP_CLI::log( __( 'Initialized a new full sync', 'jetpack' ) );
965
					}
966 View Code Duplication
				} else {
967
968
					// Reset sync settings to original.
969
					Jetpack_Sync_Settings::update_settings( $original_settings );
970
971
					if ( $modules ) {
972
						/* translators: %s is a comma separated list of Jetpack modules */
973
						WP_CLI::error( sprintf( __( 'Could not start a new full sync with modules: %s', 'jetpack' ), join( ', ', $modules ) ) );
974
					} else {
975
						WP_CLI::error( __( 'Could not start a new full sync', 'jetpack' ) );
976
					}
977
				}
978
979
				// Keep sending to WPCOM until there's nothing to send
980
				$i = 1;
981
				do {
982
					$result = Jetpack_Sync_Actions::$sender->do_full_sync();
0 ignored issues
show
The property sender cannot be accessed from this context as it is declared private in class Jetpack_Sync_Actions.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

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