PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 12.1
Jetpack – WP Security, Backup, Speed, & Growth v12.1
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 12.1, at class.jetpack-cli.php

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