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