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