Completed
Push — renovate/jsdom-16.x ( 553f2a...6948b4 )
by
unknown
77:15 queued 59:05
created

class.jetpack-cli.php (2 issues)

Upgrade to new PHP Analysis Engine

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

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
926
						'sync_wait_time'           => 0,
927
						'enqueue_wait_time'        => 0,
928
						'queue_max_writes_sec'     => 10000,
929
						'max_queue_size_full_sync' => 100000,
930
						'full_sync_send_duration'  => HOUR_IN_SECONDS,
931
					)
932
				);
933
				Settings::update_settings( $sync_settings );
0 ignored issues
show
It seems like $sync_settings defined by wp_parse_args(array_inte...n' => HOUR_IN_SECONDS)) on line 923 can also be of type null; however, Automattic\Jetpack\Sync\...ings::update_settings() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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