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

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

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