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