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

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