PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 10.9.3
Jetpack – WP Security, Backup, Speed, & Growth v10.9.3
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.jetpack-cli.php

class.jetpack-cli.php in Jetpack – WP Security, Backup, Speed, & Growth 10.9.3, at class.jetpack-cli.php

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