Completed
Push — update/sync-package ( c23cf3...c90f07 )
by
unknown
08:49
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
/**
6
 * Control your local Jetpack installation.
7
 *
8
 * Minimum PHP requirement for WP-CLI is PHP 5.3, so ignore PHP 5.2 compatibility issues.
9
 * @phpcs:disable PHPCompatibility.PHP.NewLanguageConstructs.t_ns_separatorFound
10
 */
11
class Jetpack_CLI extends WP_CLI_Command {
12
	// Aesthetics
13
	public $green_open  = "\033[32m";
14
	public $red_open    = "\033[31m";
15
	public $yellow_open = "\033[33m";
16
	public $color_close = "\033[0m";
17
18
	/**
19
	 * Get Jetpack Details
20
	 *
21
	 * ## OPTIONS
22
	 *
23
	 * empty: Leave it empty for basic stats
24
	 *
25
	 * full: View full stats.  It's the data from the heartbeat
26
	 *
27
	 * ## EXAMPLES
28
	 *
29
	 * wp jetpack status
30
	 * wp jetpack status full
31
	 *
32
	 */
33
	public function status( $args, $assoc_args ) {
34
		jetpack_require_lib( 'debugger' );
35
36
		/* translators: %s is the site URL */
37
		WP_CLI::line( sprintf( __( 'Checking status for %s', 'jetpack' ), esc_url( get_home_url() ) ) );
38
39 View Code Duplication
		if ( isset( $args[0] ) && 'full' !== $args[0] ) {
40
			/* translators: %s is a command like "prompt" */
41
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $args[0] ) );
42
		}
43
44
		$master_user_email = Jetpack::get_master_user_email();
45
46
		$cxntests = new Jetpack_Cxn_Tests();
47
48
		if ( $cxntests->pass() ) {
49
			$cxntests->output_results_for_cli();
50
51
			WP_CLI::success( __( 'Jetpack is currently connected to WordPress.com', 'jetpack' ) );
52
		} else {
53
			$error = array();
54
			foreach ( $cxntests->list_fails() as $fail ) {
55
				$error[] = $fail['name'] . ': ' . $fail['message'];
56
			}
57
			WP_CLI::error_multi_line( $error );
58
59
			$cxntests->output_results_for_cli();
60
61
			WP_CLI::error( __('Jetpack connection is broken.', 'jetpack' ) ); // Exit CLI.
62
		}
63
64
		/* translators: %s is current version of Jetpack, for example 7.3 */
65
		WP_CLI::line( sprintf( __( 'The Jetpack Version is %s', 'jetpack' ), JETPACK__VERSION ) );
66
		/* translators: %d is WP.com ID of this blog */
67
		WP_CLI::line( sprintf( __( 'The WordPress.com blog_id is %d', 'jetpack' ), Jetpack_Options::get_option( 'id' ) ) );
68
		/* translators: %s is the email address of the connection owner */
69
		WP_CLI::line( sprintf( __( 'The WordPress.com account for the primary connection is %s', 'jetpack' ), $master_user_email ) );
70
71
		/*
72
		 * Are they asking for all data?
73
		 *
74
		 * Loop through heartbeat data and organize by priority.
75
		 */
76
		$all_data = ( isset( $args[0] ) && 'full' == $args[0] ) ? 'full' : false;
77
		if ( $all_data ) {
78
			// Heartbeat data
79
			WP_CLI::line( "\n" . __( 'Additional data: ', 'jetpack' ) );
80
81
			// Get the filtered heartbeat data.
82
			// Filtered so we can color/list by severity
83
			$stats = Jetpack::jetpack_check_heartbeat_data();
84
85
			// Display red flags first
86
			foreach ( $stats['bad'] as $stat => $value ) {
87
				printf( "$this->red_open%-'.16s %s $this->color_close\n", $stat, $value );
88
			}
89
90
			// Display caution warnings next
91
			foreach ( $stats['caution'] as $stat => $value ) {
92
				printf( "$this->yellow_open%-'.16s %s $this->color_close\n", $stat, $value );
93
			}
94
95
			// The rest of the results are good!
96
			foreach ( $stats['good'] as $stat => $value ) {
97
98
				// Modules should get special spacing for aestetics
99
				if ( strpos( $stat, 'odule-' ) ) {
100
					printf( "%-'.30s %s\n", $stat, $value );
101
					usleep( 4000 ); // For dramatic effect lolz
102
					continue;
103
				}
104
				printf( "%-'.16s %s\n", $stat, $value );
105
				usleep( 4000 ); // For dramatic effect lolz
106
			}
107
		} else {
108
			// Just the basics
109
			WP_CLI::line( "\n" . _x( "View full status with 'wp jetpack status full'", '"wp jetpack status full" is a command - do not translate', 'jetpack' ) );
110
		}
111
	}
112
113
	/**
114
	 * Tests the active connection
115
	 *
116
	 * 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.
117
	 *
118
	 * ## EXAMPLES
119
	 *
120
	 * wp jetpack test-connection
121
	 *
122
	 * @subcommand test-connection
123
	 */
124
	public function test_connection( $args, $assoc_args ) {
125
126
		/* translators: %s is the site URL */
127
		WP_CLI::line( sprintf( __( 'Testing connection for %s', 'jetpack' ), esc_url( get_site_url() ) ) );
128
129
		if ( ! Jetpack::is_active() ) {
130
			WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
131
		}
132
133
		$response = Jetpack_Client::wpcom_json_api_request_as_blog(
134
			sprintf( '/jetpack-blogs/%d/test-connection', Jetpack_Options::get_option( 'id' ) ),
135
			Jetpack_Client::WPCOM_JSON_API_VERSION
136
		);
137
138 View Code Duplication
		if ( is_wp_error( $response ) ) {
139
			/* translators: %1$s is the error code, %2$s is the error message */
140
			WP_CLI::error( sprintf( __( 'Failed to test connection (#%1$s: %2$s)', 'jetpack' ), $response->get_error_code(), $response->get_error_message() ) );
141
		}
142
143
		$body = wp_remote_retrieve_body( $response );
144
		if ( ! $body ) {
145
			WP_CLI::error( __( 'Failed to test connection (empty response body)', 'jetpack' ) );
146
		}
147
148
		$result = json_decode( $body );
149
		$is_connected = (bool) $result->connected;
150
		$message = $result->message;
151
152
		if ( $is_connected ) {
153
			WP_CLI::success( $message );
154
		} else {
155
			WP_CLI::error( $message );
156
		}
157
	}
158
159
	/**
160
	 * Disconnect Jetpack Blogs or Users
161
	 *
162
	 * ## OPTIONS
163
	 *
164
	 * blog: Disconnect the entire blog.
165
	 *
166
	 * user <user_identifier>: Disconnect a specific user from WordPress.com.
167
	 *
168
	 * Please note, the primary account that the blog is connected
169
	 * to WordPress.com with cannot be disconnected without
170
	 * disconnecting the entire blog.
171
	 *
172
	 * ## EXAMPLES
173
	 *
174
	 * wp jetpack disconnect blog
175
	 * wp jetpack disconnect user 13
176
	 * wp jetpack disconnect user username
177
	 * wp jetpack disconnect user [email protected]
178
	 *
179
	 * @synopsis <blog|user> [<user_identifier>]
180
	 */
181
	public function disconnect( $args, $assoc_args ) {
182
		if ( ! Jetpack::is_active() ) {
183
			WP_CLI::error( __( 'You cannot disconnect, without having first connected.', 'jetpack' ) );
184
		}
185
186
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
187
		if ( ! in_array( $action, array( 'blog', 'user', 'prompt' ) ) ) {
188
			/* translators: %s is a command like "prompt" */
189
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
190
		}
191
192
		if ( in_array( $action, array( 'user' ) ) ) {
193
			if ( isset( $args[1] ) ) {
194
				$user_id = $args[1];
195
				if ( ctype_digit( $user_id ) ) {
196
					$field = 'id';
197
					$user_id = (int) $user_id;
198
				} elseif ( is_email( $user_id ) ) {
199
					$field = 'email';
200
					$user_id = sanitize_user( $user_id, true );
201
				} else {
202
					$field = 'login';
203
					$user_id = sanitize_user( $user_id, true );
204
				}
205
				if ( ! $user = get_user_by( $field, $user_id ) ) {
206
					WP_CLI::error( __( 'Please specify a valid user.', 'jetpack' ) );
207
				}
208
			} else {
209
				WP_CLI::error( __( 'Please specify a user by either ID, username, or email.', 'jetpack' ) );
210
			}
211
		}
212
213
		switch ( $action ) {
214
			case 'blog':
215
				Jetpack::log( 'disconnect' );
216
				Jetpack::disconnect();
217
				WP_CLI::success( sprintf(
218
					/* translators: %s is the site URL */
219
					__( 'Jetpack has been successfully disconnected for %s.', 'jetpack' ),
220
					esc_url( get_site_url() )
221
				) );
222
				break;
223
			case 'user':
224
				if ( Jetpack::unlink_user( $user->ID ) ) {
225
					Jetpack::log( 'unlink', $user->ID );
226
					WP_CLI::success( __( 'User has been successfully disconnected.', 'jetpack' ) );
227
				} else {
228
					/* translators: %s is a username */
229
					WP_CLI::error( sprintf( __( "User %s could not be disconnected. Are you sure they're connected currently?", 'jetpack' ), "{$user->login} <{$user->email}>" ) );
230
				}
231
				break;
232
			case 'prompt':
233
				WP_CLI::error( __( 'Please specify if you would like to disconnect a blog or user.', 'jetpack' ) );
234
				break;
235
		}
236
	}
237
238
	/**
239
	 * Reset Jetpack options and settings to default
240
	 *
241
	 * ## OPTIONS
242
	 *
243
	 * modules: Resets modules to default state ( get_default_modules() )
244
	 *
245
	 * options: Resets all Jetpack options except:
246
	 *  - All private options (Blog token, user token, etc...)
247
	 *  - id (The Client ID/WP.com Blog ID of this site)
248
	 *  - master_user
249
	 *  - version
250
	 *  - activated
251
	 *
252
	 * ## EXAMPLES
253
	 *
254
	 * wp jetpack reset options
255
	 * wp jetpack reset modules
256
	 * wp jetpack reset sync-checksum --dry-run --offset=0
257
	 *
258
	 * @synopsis <modules|options|sync-checksum> [--dry-run] [--offset=<offset>]
259
	 *
260
	 */
261
	public function reset( $args, $assoc_args ) {
262
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
263 View Code Duplication
		if ( ! in_array( $action, array( 'options', 'modules', 'sync-checksum' ), true ) ) {
264
			/* translators: %s is a command like "prompt" */
265
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
266
		}
267
268
		$is_dry_run = ! empty( $assoc_args['dry-run'] );
269
270 View Code Duplication
		if ( $is_dry_run ) {
271
			WP_CLI::warning(
272
				__( "\nThis is a dry run.\n", 'jetpack' ) .
273
				__( "No actions will be taken.\n", 'jetpack' ) .
274
				__( "The following messages will give you preview of what will happen when you run this command.\n\n", 'jetpack' )
275
			);
276
		} else {
277
			// We only need to confirm "Are you sure?" when we are not doing a dry run.
278
			jetpack_cli_are_you_sure();
279
		}
280
281
		switch ( $action ) {
282
			case 'options':
283
				$options_to_reset = Jetpack_Options::get_options_for_reset();
284
				// Reset the Jetpack options
285
				WP_CLI::line( sprintf(
286
					/* translators: %s is the site URL */
287
					__( "Resetting Jetpack Options for %s...\n", "jetpack" ),
288
					esc_url( get_site_url() )
289
				) );
290
				sleep(1); // Take a breath
291 View Code Duplication
				foreach ( $options_to_reset['jp_options'] as $option_to_reset ) {
292
					if ( ! $is_dry_run ) {
293
						Jetpack_Options::delete_option( $option_to_reset );
294
						usleep( 100000 );
295
					}
296
297
					/* translators: This is the result of an action. The option named %s was reset */
298
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
299
				}
300
301
				// Reset the WP options
302
				WP_CLI::line( __( "Resetting the jetpack options stored in wp_options...\n", "jetpack" ) );
303
				usleep( 500000 ); // Take a breath
304 View Code Duplication
				foreach ( $options_to_reset['wp_options'] as $option_to_reset ) {
305
					if ( ! $is_dry_run ) {
306
						delete_option( $option_to_reset );
307
						usleep( 100000 );
308
					}
309
					/* translators: This is the result of an action. The option named %s was reset */
310
					WP_CLI::success( sprintf( __( '%s option reset', 'jetpack' ), $option_to_reset ) );
311
				}
312
313
				// Reset to default modules
314
				WP_CLI::line( __( "Resetting default modules...\n", "jetpack" ) );
315
				usleep( 500000 ); // Take a breath
316
				$default_modules = Jetpack::get_default_modules();
317
				if ( ! $is_dry_run ) {
318
					Jetpack::update_active_modules( $default_modules );
319
				}
320
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
321
322
				// Jumpstart option is special
323
				if ( ! $is_dry_run ) {
324
					Jetpack_Options::update_option( 'jumpstart', 'new_connection' );
325
				}
326
				WP_CLI::success( __( 'jumpstart option reset', 'jetpack' ) );
327
				break;
328 View Code Duplication
			case 'modules':
329
				if ( ! $is_dry_run ) {
330
					$default_modules = Jetpack::get_default_modules();
331
					Jetpack::update_active_modules( $default_modules );
332
				}
333
334
				WP_CLI::success( __( 'Modules reset to default.', 'jetpack' ) );
335
				break;
336
			case 'prompt':
337
				WP_CLI::error( __( 'Please specify if you would like to reset your options, modules or sync-checksum', 'jetpack' ) );
338
				break;
339
			case 'sync-checksum':
340
				$option = 'jetpack_callables_sync_checksum';
341
342
				if ( is_multisite() ) {
343
					$offset = isset( $assoc_args['offset'] ) ? (int) $assoc_args['offset'] : 0;
344
345
					/*
346
					 * 1000 is a good limit since we don't expect the number of sites to be more than 1000
347
					 * Offset can be used to paginate and try to clean up more sites.
348
					 */
349
					$sites       = get_sites( array( 'number' => 1000, 'offset' => $offset ) );
350
					$count_fixes = 0;
351
					foreach ( $sites as $site ) {
352
						switch_to_blog( $site->blog_id );
353
						$count = self::count_option( $option );
354
						if ( $count > 1 ) {
355
							if ( ! $is_dry_run ) {
356
								delete_option( $option );
357
							}
358
							WP_CLI::line(
359
								sprintf(
360
									/* translators: %1$d is a number, %2$s is the name of an option, %2$s is the site URL. */
361
									__( 'Deleted %1$d %2$s options from %3$s', 'jetpack' ),
362
									$count,
363
									$option,
364
									"{$site->domain}{$site->path}"
365
								)
366
							);
367
							$count_fixes++;
368
							if ( ! $is_dry_run ) {
369
								/*
370
								 * We could be deleting a lot of options rows at the same time.
371
								 * Allow some time for replication to catch up.
372
								 */
373
								sleep( 3 );
374
							}
375
						}
376
377
						restore_current_blog();
378
					}
379
					if ( $count_fixes ) {
380
						WP_CLI::success(
381
							sprintf(
382
								/* translators: %1$s is the name of an option, %2$d is a number of sites. */
383
								__( 'Successfully reset %1$s on %2$d sites.', 'jetpack' ),
384
								$option,
385
								$count_fixes
386
							)
387
						);
388
					} else {
389
						WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
390
					}
391
					return;
392
				}
393
394
				$count = self::count_option( $option );
395
				if ( $count > 1 ) {
396
					if ( ! $is_dry_run ) {
397
						delete_option( $option );
398
					}
399
					WP_CLI::success(
400
						sprintf(
401
							/* translators: %1$d is a number, %2$s is the name of an option. */
402
							__( 'Deleted %1$d %2$s options', 'jetpack' ),
403
							$count,
404
							$option
405
						)
406
					);
407
					return;
408
				}
409
410
				WP_CLI::success( __( 'No options were deleted.', 'jetpack' ) );
411
				break;
412
413
		}
414
	}
415
416
	/**
417
	 * Return the number of times an option appears
418
	 * Normally an option would only appear 1 since the option key is supposed to be unique
419
	 * but if a site hasn't updated the DB schema then that would not be the case.
420
	 *
421
	 * @param string $option Option name.
422
	 *
423
	 * @return int
424
	 */
425
	private static function count_option( $option ) {
426
		global $wpdb;
427
		return (int) $wpdb->get_var(
428
			$wpdb->prepare(
429
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name = %s",
430
				$option
431
			)
432
		);
433
434
	}
435
436
	/**
437
	 * Manage Jetpack Modules
438
	 *
439
	 * ## OPTIONS
440
	 *
441
	 * <list|activate|deactivate|toggle>
442
	 * : The action to take.
443
	 * ---
444
	 * default: list
445
	 * options:
446
	 *  - list
447
	 *  - activate
448
	 *  - deactivate
449
	 *  - toggle
450
	 * ---
451
	 *
452
	 * [<module_slug>]
453
	 * : The slug of the module to perform an action on.
454
	 *
455
	 * [--format=<format>]
456
	 * : Allows overriding the output of the command when listing modules.
457
	 * ---
458
	 * default: table
459
	 * options:
460
	 *  - table
461
	 *  - json
462
	 *  - csv
463
	 *  - yaml
464
	 *  - ids
465
	 *  - count
466
	 * ---
467
	 *
468
	 * ## EXAMPLES
469
	 *
470
	 * wp jetpack module list
471
	 * wp jetpack module list --format=json
472
	 * wp jetpack module activate stats
473
	 * wp jetpack module deactivate stats
474
	 * wp jetpack module toggle stats
475
	 * wp jetpack module activate all
476
	 * wp jetpack module deactivate all
477
	 */
478
	public function module( $args, $assoc_args ) {
479
		$action = isset( $args[0] ) ? $args[0] : 'list';
480
481
		if ( isset( $args[1] ) ) {
482
			$module_slug = $args[1];
483
			if ( 'all' !== $module_slug && ! Jetpack::is_module( $module_slug ) ) {
484
				/* translators: %s is a module slug like "stats" */
485
				WP_CLI::error( sprintf( __( '%s is not a valid module.', 'jetpack' ), $module_slug ) );
486
			}
487
			if ( 'toggle' === $action ) {
488
				$action = Jetpack::is_module_active( $module_slug )
489
					? 'deactivate'
490
					: 'activate';
491
			}
492
			if ( 'all' === $args[1] ) {
493
				$action = ( 'deactivate' === $action )
494
					? 'deactivate_all'
495
					: 'activate_all';
496
			}
497
		} elseif ( 'list' !== $action ) {
498
			WP_CLI::line( __( 'Please specify a valid module.', 'jetpack' ) );
499
			$action = 'list';
500
		}
501
502
		switch ( $action ) {
503
			case 'list':
504
				$modules_list = array();
505
				$modules      = Jetpack::get_available_modules();
506
				sort( $modules );
507
				foreach ( (array) $modules as $module_slug ) {
508
					if ( 'vaultpress' === $module_slug ) {
509
						continue;
510
					}
511
					$modules_list[] = array(
512
						'slug'   => $module_slug,
513
						'status' => Jetpack::is_module_active( $module_slug )
514
							? __( 'Active', 'jetpack' )
515
							: __( 'Inactive', 'jetpack' ),
516
					);
517
				}
518
				WP_CLI\Utils\format_items( $assoc_args['format'], $modules_list, array( 'slug', 'status' ) );
519
				break;
520
			case 'activate':
521
				$module = Jetpack::get_module( $module_slug );
522
				Jetpack::log( 'activate', $module_slug );
523
				if ( Jetpack::activate_module( $module_slug, false, false ) ) {
524
					/* translators: %s is the name of a Jetpack module */
525
					WP_CLI::success( sprintf( __( '%s has been activated.', 'jetpack' ), $module['name'] ) );
526
				} else {
527
					/* translators: %s is the name of a Jetpack module */
528
					WP_CLI::error( sprintf( __( '%s could not be activated.', 'jetpack' ), $module['name'] ) );
529
				}
530
				break;
531 View Code Duplication
			case 'activate_all':
532
				$modules = Jetpack::get_available_modules();
533
				Jetpack::update_active_modules( $modules );
534
				WP_CLI::success( __( 'All modules activated!', 'jetpack' ) );
535
				break;
536
			case 'deactivate':
537
				$module = Jetpack::get_module( $module_slug );
538
				Jetpack::log( 'deactivate', $module_slug );
539
				Jetpack::deactivate_module( $module_slug );
540
				/* translators: %s is the name of a Jetpack module */
541
				WP_CLI::success( sprintf( __( '%s has been deactivated.', 'jetpack' ), $module['name'] ) );
542
				break;
543
			case 'deactivate_all':
544
				Jetpack::delete_active_modules();
545
				WP_CLI::success( __( 'All modules deactivated!', 'jetpack' ) );
546
				break;
547
			case 'toggle':
548
				// Will never happen, should have been handled above and changed to activate or deactivate.
549
				break;
550
		}
551
	}
552
553
	/**
554
	 * Manage Protect Settings
555
	 *
556
	 * ## OPTIONS
557
	 *
558
	 * whitelist: Whitelist an IP address.  You can also read or clear the whitelist.
559
	 *
560
	 *
561
	 * ## EXAMPLES
562
	 *
563
	 * wp jetpack protect whitelist <ip address>
564
	 * wp jetpack protect whitelist list
565
	 * wp jetpack protect whitelist clear
566
	 *
567
	 * @synopsis <whitelist> [<ip|ip_low-ip_high|list|clear>]
568
	 */
569
	public function protect( $args, $assoc_args ) {
570
		$action = isset( $args[0] ) ? $args[0] : 'prompt';
571
		if ( ! in_array( $action, array( 'whitelist' ) ) ) {
572
			/* translators: %s is a command like "prompt" */
573
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
574
		}
575
		// Check if module is active
576
		if ( ! Jetpack::is_module_active( __FUNCTION__ ) ) {
577
			/* translators: %s is a module name */
578
			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__ ) );
579
		}
580
		if ( in_array( $action, array( 'whitelist' ) ) ) {
581
			if ( isset( $args[1] ) ) {
582
				$action = 'whitelist';
583
			} else {
584
				$action = 'prompt';
585
			}
586
		}
587
		switch ( $action ) {
588
			case 'whitelist':
589
				$whitelist         = array();
590
				$new_ip            = $args[1];
591
				$current_whitelist = get_site_option( 'jetpack_protect_whitelist', array() );
592
593
				// Build array of IPs that are already whitelisted.
594
				// Re-build manually instead of using jetpack_protect_format_whitelist() so we can easily get
595
				// low & high range params for jetpack_protect_ip_address_is_in_range();
596
				foreach( $current_whitelist as $whitelisted ) {
597
598
					// IP ranges
599
					if ( $whitelisted->range ) {
600
601
						// Is it already whitelisted?
602
						if ( jetpack_protect_ip_address_is_in_range( $new_ip, $whitelisted->range_low, $whitelisted->range_high ) ) {
603
							/* translators: %s is an IP address */
604
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
605
							break;
606
						}
607
						$whitelist[] = $whitelisted->range_low . " - " . $whitelisted->range_high;
608
609
					} else { // Individual IPs
610
611
						// Check if the IP is already whitelisted (single IP only)
612
						if ( $new_ip == $whitelisted->ip_address ) {
613
							/* translators: %s is an IP address */
614
							WP_CLI::error( sprintf( __( '%s has already been whitelisted', 'jetpack' ), $new_ip ) );
615
							break;
616
						}
617
						$whitelist[] = $whitelisted->ip_address;
618
619
					}
620
				}
621
622
				/*
623
				 * List the whitelist
624
				 * Done here because it's easier to read the $whitelist array after it's been rebuilt
625
				 */
626
				if ( isset( $args[1] ) && 'list' == $args[1] ) {
627
					if ( ! empty( $whitelist ) ) {
628
						WP_CLI::success( __( 'Here are your whitelisted IPs:', 'jetpack' ) );
629
						foreach ( $whitelist as $ip ) {
630
							WP_CLI::line( "\t" . str_pad( $ip, 24 ) ) ;
631
						}
632
					} else {
633
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
634
					}
635
					break;
636
				}
637
638
				/*
639
				 * Clear the whitelist
640
				 */
641
				if ( isset( $args[1] ) && 'clear' == $args[1] ) {
642
					if ( ! empty( $whitelist ) ) {
643
						$whitelist = array();
644
						jetpack_protect_save_whitelist( $whitelist );
645
						WP_CLI::success( __( 'Cleared all whitelisted IPs', 'jetpack' ) );
646
					} else {
647
						WP_CLI::line( __( 'Whitelist is empty.', "jetpack" ) ) ;
648
					}
649
					break;
650
				}
651
652
				// Append new IP to whitelist array
653
				array_push( $whitelist, $new_ip );
654
655
				// Save whitelist if there are no errors
656
				$result = jetpack_protect_save_whitelist( $whitelist );
657
				if ( is_wp_error( $result ) ) {
658
					WP_CLI::error( $result );
659
				}
660
661
				/* translators: %s is an IP address */
662
				WP_CLI::success( sprintf( __( '%s has been whitelisted.', 'jetpack' ), $new_ip ) );
663
				break;
664
			case 'prompt':
665
				WP_CLI::error(
666
					__( 'No command found.', 'jetpack' ) . "\n" .
667
					__( 'Please enter the IP address you want to whitelist.', 'jetpack' ) . "\n" .
668
					_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" .
669
					_x( "You can also 'list' or 'clear' the whitelist.", "'list' and 'clear' are commands and should not be translated", 'jetpack' ) . "\n"
670
				);
671
				break;
672
		}
673
	}
674
675
	/**
676
	 * Manage Jetpack Options
677
	 *
678
	 * ## OPTIONS
679
	 *
680
	 * list   : List all jetpack options and their values
681
	 * delete : Delete an option
682
	 *          - can only delete options that are white listed.
683
	 * update : update an option
684
	 *          - can only update option strings
685
	 * get    : get the value of an option
686
	 *
687
	 * ## EXAMPLES
688
	 *
689
	 * wp jetpack options list
690
	 * wp jetpack options get    <option_name>
691
	 * wp jetpack options delete <option_name>
692
	 * wp jetpack options update <option_name> [<option_value>]
693
	 *
694
	 * @synopsis <list|get|delete|update> [<option_name>] [<option_value>]
695
	 */
696
	public function options( $args, $assoc_args ) {
697
		$action = isset( $args[0] ) ? $args[0] : 'list';
698
		$safe_to_modify = Jetpack_Options::get_options_for_reset();
699
700
		// Jumpstart is special
701
		array_push( $safe_to_modify, 'jumpstart' );
702
703
		// Is the option flagged as unsafe?
704
		$flagged = ! in_array( $args[1], $safe_to_modify );
705
706 View Code Duplication
		if ( ! in_array( $action, array( 'list', 'get', 'delete', 'update' ) ) ) {
707
			/* translators: %s is a command like "prompt" */
708
			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'jetpack' ), $action ) );
709
		}
710
711
		if ( isset( $args[0] ) ) {
712
			if ( 'get' == $args[0] && isset( $args[1] ) ) {
713
				$action = 'get';
714
			} else if ( 'delete' == $args[0] && isset( $args[1] ) ) {
715
				$action = 'delete';
716 View Code Duplication
			} else if ( 'update' == $args[0] && isset( $args[1] ) ) {
717
				$action = 'update';
718
			} else {
719
				$action = 'list';
720
			}
721
		}
722
723
		// Bail if the option isn't found
724
		$option = isset( $args[1] ) ? Jetpack_Options::get_option( $args[1] ) : false;
725 View Code Duplication
		if ( isset( $args[1] ) && ! $option && 'update' !== $args[0] ) {
726
			WP_CLI::error( __( 'Option not found or is empty.  Use "list" to list option names', 'jetpack' ) );
727
		}
728
729
		// Let's print_r the option if it's an array
730
		// Used in the 'get' and 'list' actions
731
		$option = is_array( $option ) ? print_r( $option ) : $option;
732
733
		switch ( $action ) {
734
			case 'get':
735
				WP_CLI::success( "\t" . $option );
736
				break;
737
			case 'delete':
738
				jetpack_cli_are_you_sure( $flagged );
739
740
				Jetpack_Options::delete_option( $args[1] );
741
				/* translators: %s is the option name */
742
				WP_CLI::success( sprintf( __( 'Deleted option: %s', 'jetpack' ), $args[1] ) );
743
				break;
744
			case 'update':
745
				jetpack_cli_are_you_sure( $flagged );
746
747
				// Updating arrays would get pretty tricky...
748
				$value = Jetpack_Options::get_option( $args[1] );
749
				if ( $value && is_array( $value ) ) {
750
					WP_CLI::error( __( 'Sorry, no updating arrays at this time', 'jetpack' ) );
751
				}
752
753
				Jetpack_Options::update_option( $args[1], $args[2] );
754
				/* translators: %1$s is the previous value, %2$s is the new value */
755
				WP_CLI::success( sprintf( _x( 'Updated option: %1$s to "%2$s"', 'Updating an option from "this" to "that".', 'jetpack' ), $args[1], $args[2] ) );
756
				break;
757
			case 'list':
758
				$options_compact     = Jetpack_Options::get_option_names();
759
				$options_non_compact = Jetpack_Options::get_option_names( 'non_compact' );
760
				$options_private     = Jetpack_Options::get_option_names( 'private' );
761
				$options             = array_merge( $options_compact, $options_non_compact, $options_private );
762
763
				// Table headers
764
				WP_CLI::line( "\t" . str_pad( __( 'Option', 'jetpack' ), 30 ) . __( 'Value', 'jetpack' ) );
765
766
				// List out the options and their values
767
				// Tell them if the value is empty or not
768
				// Tell them if it's an array
769
				foreach ( $options as $option ) {
770
					$value = Jetpack_Options::get_option( $option );
771
					if ( ! $value ) {
772
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Empty' );
773
						continue;
774
					}
775
776
					if ( ! is_array( $value ) ) {
777
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . $value );
778
					} else if ( is_array( $value ) ) {
779
						WP_CLI::line( "\t" . str_pad( $option, 30 ) . 'Array - Use "get <option>" to read option array.' );
780
					}
781
				}
782
				$option_text = '{' . _x( 'option', 'a variable command that a user can write, provided in the printed instructions', 'jetpack' ) . '}';
783
				$value_text  = '{' . _x( 'value', 'the value that they want to update the option to', 'jetpack' ) . '}';
784
785
				WP_CLI::success(
786
					_x( "Above are your options. You may 'get', 'delete', and 'update' them.", "'get', 'delete', and 'update' are commands - do not translate.", 'jetpack' ) . "\n" .
787
					str_pad( 'wp jetpack options get', 26 )    . $option_text . "\n" .
788
					str_pad( 'wp jetpack options delete', 26 ) . $option_text . "\n" .
789
					str_pad( 'wp jetpack options update', 26 ) . "$option_text $value_text" . "\n" .
790
					_x( "Type 'wp jetpack options' for more info.", "'wp jetpack options' is a command - do not translate.", 'jetpack' ) . "\n"
791
				);
792
				break;
793
		}
794
	}
795
796
	/**
797
	 * Get the status of or start a new Jetpack sync.
798
	 *
799
	 * ## OPTIONS
800
	 *
801
	 * status   : Print the current sync status
802
	 * settings : Prints the current sync settings
803
	 * start    : Start a full sync from this site to WordPress.com
804
	 * enable   : Enables sync on the site
805
	 * disable  : Disable sync on a site
806
	 * reset    : Disables sync and Resets the sync queues on a site
807
	 *
808
	 * ## EXAMPLES
809
	 *
810
	 * wp jetpack sync status
811
	 * wp jetpack sync settings
812
	 * wp jetpack sync start --modules=functions --sync_wait_time=5
813
	 * wp jetpack sync enable
814
	 * wp jetpack sync disable
815
	 * wp jetpack sync reset
816
	 * wp jetpack sync reset --queue=full or regular
817
	 *
818
	 * @synopsis <status|start> [--<field>=<value>]
819
	 */
820
	public function sync( $args, $assoc_args ) {
821
822
		$action = isset( $args[0] ) ? $args[0] : 'status';
823
824
		switch ( $action ) {
825
			case 'status':
826
				$status = Jetpack_Sync_Actions::get_sync_status();
827
				$collection = array();
828
				foreach ( $status as $key => $item ) {
829
					$collection[]  = array(
830
						'option' => $key,
831
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
832
					);
833
				}
834
				WP_CLI::log( __( 'Sync Status:', 'jetpack' ) );
835
				WP_CLI\Utils\format_items( 'table', $collection, array( 'option', 'value' ) );
836
				break;
837
			case 'settings':
838
				WP_CLI::log( __( 'Sync Settings:', 'jetpack' ) );
839
				foreach( Jetpack_Sync_Settings::get_settings() as $setting => $item ) {
840
					$settings[]  = array(
841
						'setting' => $setting,
842
						'value' => is_scalar( $item ) ? $item : json_encode( $item )
843
					);
844
				}
845
				WP_CLI\Utils\format_items( 'table', $settings, array( 'setting', 'value' ) );
846
847
			case 'disable':
848
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
849
				update_option( 'jetpack_sync_settings_disable', 1 );
850
				/* translators: %s is the site URL */
851
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s', 'jetpack' ), get_site_url() ) );
852
				break;
853
			case 'enable':
854
				Jetpack_Sync_Settings::update_settings( array( 'disable' => 0 ) );
855
				/* translators: %s is the site URL */
856
				WP_CLI::log( sprintf( __( 'Sync Enabled on %s', 'jetpack' ), get_site_url() ) );
857
				break;
858
			case 'reset':
859
				// Don't set it via the Jetpack_Sync_Settings since that also resets the queues.
860
				update_option( 'jetpack_sync_settings_disable', 1 );
861
862
				/* translators: %s is the site URL */
863
				WP_CLI::log( sprintf( __( 'Sync Disabled on %s. Use `wp jetpack sync enable` to enable syncing again.', 'jetpack' ), get_site_url() ) );
864
				$listener = Jetpack_Sync_Listener::get_instance();
0 ignored issues
show
The method get_instance() does not seem to exist on object<Jetpack_Sync_Listener>.

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

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

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