PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 15.8-beta
Jetpack – WP Security, Backup, Speed, & Growth v15.8-beta
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 2 months ago _inc 4 weeks ago css 4 weeks ago extensions 4 weeks ago images 1 month ago jetpack_vendor 4 weeks ago json-endpoints 4 weeks ago modules 4 weeks ago sal 4 weeks ago src 4 weeks ago vendor 4 weeks ago views 1 month ago CHANGELOG.md 4 weeks ago LICENSE.txt 5 months ago SECURITY.md 2 years 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 6 months ago class.frame-nonce-preview.php 6 months ago class.jetpack-admin.php 1 month ago class.jetpack-autoupdate.php 6 months ago class.jetpack-cli.php 5 months ago class.jetpack-client-server.php 2 years ago class.jetpack-gutenberg.php 2 months 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 4 weeks ago class.json-api-endpoints.php 1 month ago class.json-api.php 5 months ago class.photon.php 3 years ago composer.json 4 weeks ago enhanced-open-graph.php 3 months ago functions.compat.php 3 months ago functions.cookies.php 2 years ago functions.global.php 1 month ago functions.is-mobile.php 2 years ago functions.opengraph.php 2 months ago functions.photon.php 2 years ago jetpack.php 4 weeks ago json-api-config.php 3 years ago json-endpoints.php 2 years ago load-jetpack.php 2 months ago locales.php 6 months ago readme.txt 4 weeks ago unauth-file-upload.php 6 months ago uninstall.php 6 months ago wpml-config.xml 3 years ago
class.jetpack-cli.php
2222 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 Jetpack_Cxn_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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $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 = isset( $args[0] ) ? $args[0] : 'sync';
1113 $action = isset( $args[1] ) ? $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 = isset( $queue_name_map[ $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 = isset( $token->message )
1278 ? $token->message
1279 : '';
1280 $this->partner_provision_error( new WP_Error( $token->error, $message ) );
1281 }
1282
1283 if ( ! isset( $token->access_token ) ) {
1284 $this->partner_provision_error( new WP_Error( 'missing_access_token', __( 'Missing or invalid access token', 'jetpack' ) ) );
1285 }
1286
1287 require_once JETPACK__PLUGIN_DIR . '_inc/class.jetpack-provision.php';
1288
1289 $body_json = Jetpack_Provision::partner_provision( $token->access_token, $named_args );
1290
1291 if ( is_wp_error( $body_json ) ) {
1292 WP_CLI::error(
1293 wp_json_encode(
1294 array(
1295 'success' => false,
1296 'error_code' => $body_json->get_error_code(),
1297 'error_message' => $body_json->get_error_message(),
1298 ),
1299 JSON_UNESCAPED_SLASHES
1300 )
1301 );
1302 exit( 1 );
1303 }
1304
1305 WP_CLI::log( wp_json_encode( $body_json, JSON_UNESCAPED_SLASHES ) );
1306 }
1307
1308 /**
1309 * Manages your Jetpack sitemap
1310 *
1311 * ## OPTIONS
1312 *
1313 * rebuild : Rebuild all sitemaps
1314 * --purge : if set, will remove all existing sitemap data before rebuilding
1315 * --monitor : if set, will output elapsed time, peak memory usage, CPU time (user/system), and average CPU utilization
1316 * --suspend-cache-addition : if set, will suspend cache additions during sitemap generation
1317 *
1318 * ## EXAMPLES
1319 *
1320 * wp jetpack sitemap rebuild
1321 * wp jetpack sitemap rebuild --monitor
1322 *
1323 * @subcommand sitemap
1324 * @synopsis <rebuild> [--purge] [--monitor] [--suspend-cache-addition]
1325 *
1326 * @param array $args Positional args.
1327 * @param array $assoc_args Named args.
1328 */
1329 public function sitemap( $args, $assoc_args ) {
1330 if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1331 WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1332 }
1333 if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1334 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' ) );
1335 }
1336
1337 if ( isset( $assoc_args['suspend-cache-addition'] ) && $assoc_args['suspend-cache-addition'] ) {
1338 add_filter( 'jetpack_sitemap_suspend_cache_addition', '__return_true' );
1339 WP_CLI::success( 'Suspending cache addition.' );
1340 }
1341
1342 $monitor = isset( $assoc_args['monitor'] ) && $assoc_args['monitor'];
1343
1344 if ( $monitor ) {
1345 $start_time = microtime( true );
1346 $rusage_start = function_exists( 'getrusage' ) ? getrusage() : null;
1347 }
1348
1349 if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1350 $librarian = new Jetpack_Sitemap_Librarian();
1351 $librarian->delete_all_stored_sitemap_data();
1352
1353 // Clear sitemap-related transients
1354 delete_transient( 'jetpack_news_sitemap_xml' );
1355 delete_transient( 'jetpack-sitemap-state-lock' );
1356 WP_CLI::success( __( 'Purged all sitemap data and cleared sitemap transients.', 'jetpack' ) );
1357 }
1358
1359 $sitemap_builder = new Jetpack_Sitemap_Builder();
1360 $sitemap_builder->update_sitemap();
1361
1362 WP_CLI::success( __( 'Sitemap rebuilt successfully.', 'jetpack' ) );
1363
1364 if ( $monitor && isset( $start_time ) ) {
1365 $end_time = microtime( true );
1366 $peak_memory = memory_get_peak_usage();
1367 $elapsed_time = $end_time - $start_time;
1368 $rusage_end = function_exists( 'getrusage' ) ? getrusage() : null;
1369
1370 WP_CLI::log( '----------------------------------' );
1371 WP_CLI::log( __( 'Performance Metrics:', 'jetpack' ) );
1372 /* translators: %s is a float representing seconds */
1373 WP_CLI::log( sprintf( __( 'Elapsed Time: %.4f seconds', 'jetpack' ), $elapsed_time ) );
1374 /* translators: %s is a human-readable memory size (e.g., 128MB) */
1375 WP_CLI::log( sprintf( __( 'Peak Memory Usage: %s', 'jetpack' ), size_format( $peak_memory ) ) );
1376
1377 if ( ! empty( $rusage_start ) && ! empty( $rusage_end ) ) {
1378 $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'] );
1379 $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'] );
1380
1381 /* translators: %d is an integer representing microseconds */
1382 WP_CLI::log( sprintf( __( 'CPU time (user): %d microseconds', 'jetpack' ), $user_cpu_time ) );
1383 /* translators: %d is an integer representing microseconds */
1384 WP_CLI::log( sprintf( __( 'CPU time (system): %d microseconds', 'jetpack' ), $system_cpu_time ) );
1385
1386 // Average CPU utilization over the elapsed wall time.
1387 $total_cpu_sec = ( $user_cpu_time + $system_cpu_time ) / 1e6;
1388 $avg_cpu_pct = $elapsed_time > 0 ? ( $total_cpu_sec / $elapsed_time ) * 100 : 0.0;
1389 /* translators: %s is a percentage like 83.4 */
1390 WP_CLI::log( sprintf( __( 'Average CPU Utilization: %.1f%%', 'jetpack' ), $avg_cpu_pct ) );
1391 }
1392 WP_CLI::log( '----------------------------------' );
1393 }
1394 }
1395
1396 /**
1397 * Allows authorizing a user via the command line and will activate
1398 *
1399 * ## EXAMPLES
1400 *
1401 * wp jetpack authorize_user --token=123456789abcdef
1402 *
1403 * @synopsis --token=<value>
1404 *
1405 * @param array $args Positional args.
1406 * @param array $named_args Named args.
1407 */
1408 public function authorize_user( $args, $named_args ) {
1409 if ( ! is_user_logged_in() ) {
1410 WP_CLI::error( __( 'Please select a user to authorize via the --user global argument.', 'jetpack' ) );
1411 }
1412
1413 if ( empty( $named_args['token'] ) ) {
1414 WP_CLI::error( __( 'A non-empty token argument must be passed.', 'jetpack' ) );
1415 }
1416
1417 $is_connection_owner = ! Jetpack::connection()->has_connected_owner();
1418 $current_user_id = get_current_user_id();
1419
1420 ( new Tokens() )->update_user_token( $current_user_id, sprintf( '%s.%d', $named_args['token'], $current_user_id ), $is_connection_owner );
1421
1422 WP_CLI::log( wp_json_encode( $named_args, JSON_UNESCAPED_SLASHES ) );
1423
1424 if ( $is_connection_owner ) {
1425 /**
1426 * Auto-enable SSO module for new Jetpack Start connections
1427 *
1428 * @since 5.0.0
1429 *
1430 * @param bool $enable_sso Whether to enable the SSO module. Default to true.
1431 */
1432 $enable_sso = apply_filters( 'jetpack_start_enable_sso', true );
1433 Jetpack::handle_post_authorization_actions( $enable_sso, false );
1434
1435 /* translators: %d is a user ID */
1436 WP_CLI::success( sprintf( __( 'Authorized %d and activated default modules.', 'jetpack' ), $current_user_id ) );
1437 } else {
1438 /* translators: %d is a user ID */
1439 WP_CLI::success( sprintf( __( 'Authorized %d.', 'jetpack' ), $current_user_id ) );
1440 }
1441 }
1442
1443 /**
1444 * Allows calling a WordPress.com API endpoint using the current blog's token.
1445 *
1446 * ## OPTIONS
1447 * --resource=<resource>
1448 * : The resource to call with the current blog's token, where `%d` represents the current blog's ID.
1449 *
1450 * [--api_version=<api_version>]
1451 * : The API version to query against.
1452 *
1453 * [--base_api_path=<base_api_path>]
1454 * : The base API path to query.
1455 * ---
1456 * default: rest
1457 * ---
1458 *
1459 * [--body=<body>]
1460 * : A JSON encoded string representing arguments to send in the body.
1461 *
1462 * [--field=<value>]
1463 * : Any number of arguments that should be passed to the resource.
1464 *
1465 * [--pretty]
1466 * : Will pretty print the results of a successful API call.
1467 *
1468 * [--strip-success]
1469 * : Will remove the green success label from successful API calls.
1470 *
1471 * ## EXAMPLES
1472 *
1473 * wp jetpack call_api --resource='/sites/%d'
1474 *
1475 * @param array $args Positional args.
1476 * @param array $named_args Named args.
1477 */
1478 public function call_api( $args, $named_args ) {
1479 if ( ! Jetpack::is_connection_ready() ) {
1480 WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1481 }
1482
1483 $consumed_args = array(
1484 'resource',
1485 'api_version',
1486 'base_api_path',
1487 'body',
1488 'pretty',
1489 );
1490
1491 // Get args that should be passed to resource.
1492 $other_args = array_diff_key( $named_args, array_flip( $consumed_args ) );
1493
1494 $decoded_body = ! empty( $named_args['body'] )
1495 ? json_decode( $named_args['body'], true )
1496 : false;
1497
1498 $resource_url = ( ! str_contains( $named_args['resource'], '%d' ) )
1499 ? $named_args['resource']
1500 : sprintf( $named_args['resource'], Jetpack_Options::get_option( 'id' ) );
1501
1502 $response = Client::wpcom_json_api_request_as_blog(
1503 $resource_url,
1504 empty( $named_args['api_version'] ) ? Client::WPCOM_JSON_API_VERSION : $named_args['api_version'],
1505 $other_args,
1506 empty( $decoded_body ) ? null : $decoded_body,
1507 empty( $named_args['base_api_path'] ) ? 'rest' : $named_args['base_api_path']
1508 );
1509
1510 if ( is_wp_error( $response ) ) {
1511 WP_CLI::error(
1512 sprintf(
1513 /* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an error code, %3$s is an error message. */
1514 __( 'Request to %1$s returned an error: (%2$d) %3$s.', 'jetpack' ),
1515 $resource_url,
1516 $response->get_error_code(),
1517 $response->get_error_message()
1518 )
1519 );
1520 }
1521
1522 if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
1523 WP_CLI::error(
1524 sprintf(
1525 /* translators: %1$s is an endpoint route (ex. /sites/123456), %2$d is an HTTP status code. */
1526 __( 'Request to %1$s returned a non-200 response code: %2$d.', 'jetpack' ),
1527 $resource_url,
1528 wp_remote_retrieve_response_code( $response )
1529 )
1530 );
1531 }
1532
1533 $output = wp_remote_retrieve_body( $response );
1534 if ( isset( $named_args['pretty'] ) ) {
1535 $decoded_output = json_decode( $output );
1536 if ( $decoded_output ) {
1537 $output = wp_json_encode( $decoded_output, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT );
1538 }
1539 }
1540
1541 if ( isset( $named_args['strip-success'] ) ) {
1542 WP_CLI::log( $output );
1543 WP_CLI::halt( 0 );
1544 }
1545
1546 WP_CLI::success( $output );
1547 }
1548
1549 /**
1550 * Allows uploading SSH Credentials to the current site for backups, restores, and security scanning.
1551 *
1552 * ## OPTIONS
1553 *
1554 * [--host=<host>]
1555 * : The SSH server's address.
1556 *
1557 * [--ssh-user=<user>]
1558 * : The username to use to log in to the SSH server.
1559 *
1560 * [--pass=<pass>]
1561 * : The password used to log in, if using a password. (optional)
1562 *
1563 * [--kpri=<kpri>]
1564 * : The private key used to log in, if using a private key. (optional)
1565 *
1566 * [--pretty]
1567 * : Will pretty print the results of a successful API call. (optional)
1568 *
1569 * [--strip-success]
1570 * : Will remove the green success label from successful API calls. (optional)
1571 *
1572 * ## EXAMPLES
1573 *
1574 * wp jetpack upload_ssh_creds --host=example.com --ssh-user=example --pass=password
1575 * wp jetpack updload_ssh_creds --host=example.com --ssh-user=example --kpri=key
1576 *
1577 * @param array $args Positional args.
1578 * @param array $named_args Named args.
1579 */
1580 public function upload_ssh_creds( $args, $named_args ) {
1581 if ( ! Jetpack::is_connection_ready() ) {
1582 WP_CLI::error( __( 'Jetpack is not currently connected to WordPress.com', 'jetpack' ) );
1583 }
1584
1585 $required_args = array(
1586 'host',
1587 'ssh-user',
1588 );
1589
1590 foreach ( $required_args as $arg ) {
1591 if ( empty( $named_args[ $arg ] ) ) {
1592 WP_CLI::error(
1593 sprintf(
1594 /* translators: %s is a slug, such as 'host'. */
1595 __( '`%s` cannot be empty.', 'jetpack' ),
1596 $arg
1597 )
1598 );
1599 }
1600 }
1601
1602 if ( empty( $named_args['pass'] ) && empty( $named_args['kpri'] ) ) {
1603 WP_CLI::error( __( 'Both `pass` and `kpri` fields cannot be blank.', 'jetpack' ) );
1604 }
1605
1606 $values = array(
1607 'credentials' => array(
1608 'site_url' => get_site_url(),
1609 'abspath' => ABSPATH,
1610 'protocol' => 'ssh',
1611 'port' => 22,
1612 'role' => 'main',
1613 'host' => $named_args['host'],
1614 'user' => $named_args['ssh-user'],
1615 'pass' => empty( $named_args['pass'] ) ? '' : $named_args['pass'],
1616 'kpri' => empty( $named_args['kpri'] ) ? '' : $named_args['kpri'],
1617 ),
1618 );
1619
1620 $named_args = wp_parse_args(
1621 array(
1622 'resource' => '/activity-log/%d/update-credentials',
1623 'method' => 'POST',
1624 'api_version' => '1.1',
1625 'body' => wp_json_encode( $values, JSON_UNESCAPED_SLASHES ),
1626 'timeout' => 30,
1627 ),
1628 $named_args
1629 );
1630
1631 self::call_api( $args, $named_args );
1632 }
1633
1634 /**
1635 * API wrapper for getting stats from the WordPress.com API for the current site.
1636 *
1637 * ## OPTIONS
1638 *
1639 * [--quantity=<quantity>]
1640 * : The number of units to include.
1641 * ---
1642 * default: 30
1643 * ---
1644 *
1645 * [--period=<period>]
1646 * : The unit of time to query stats for.
1647 * ---
1648 * default: day
1649 * options:
1650 * - day
1651 * - week
1652 * - month
1653 * - year
1654 * ---
1655 *
1656 * [--date=<date>]
1657 * : The latest date to return stats for. Ex. - 2018-01-01.
1658 *
1659 * [--pretty]
1660 * : Will pretty print the results of a successful API call.
1661 *
1662 * [--strip-success]
1663 * : Will remove the green success label from successful API calls.
1664 *
1665 * ## EXAMPLES
1666 *
1667 * wp jetpack get_stats
1668 *
1669 * @param array $args Positional args.
1670 * @param array $named_args Named args.
1671 */
1672 public function get_stats( $args, $named_args ) {
1673 $selected_args = array_intersect_key(
1674 $named_args,
1675 array_flip(
1676 array(
1677 'quantity',
1678 'date',
1679 )
1680 )
1681 );
1682
1683 // The API expects unit, but period seems to be more correct.
1684 $selected_args['unit'] = $named_args['period'];
1685
1686 $command = sprintf(
1687 'jetpack call_api --resource=/sites/%d/stats/%s',
1688 Jetpack_Options::get_option( 'id' ),
1689 add_query_arg( $selected_args, 'visits' )
1690 );
1691
1692 if ( isset( $named_args['pretty'] ) ) {
1693 $command .= ' --pretty';
1694 }
1695
1696 if ( isset( $named_args['strip-success'] ) ) {
1697 $command .= ' --strip-success';
1698 }
1699
1700 WP_CLI::runcommand(
1701 $command,
1702 array(
1703 'launch' => false, // Use the current process.
1704 )
1705 );
1706 }
1707
1708 /**
1709 * Allows management of publicize connections.
1710 *
1711 * ## OPTIONS
1712 *
1713 * <list|disconnect>
1714 * : The action to perform.
1715 * ---
1716 * options:
1717 * - list
1718 * - disconnect
1719 * ---
1720 *
1721 * [<identifier>]
1722 * : The connection ID or service to perform an action on.
1723 *
1724 * [--ignore-cache]
1725 * : Whether to ignore connections cache.
1726 *
1727 * [--format=<format>]
1728 * : Allows overriding the output of the command when listing connections.
1729 * ---
1730 * default: table
1731 * options:
1732 * - table
1733 * - json
1734 * - csv
1735 * - yaml
1736 * - ids
1737 * - count
1738 * ---
1739 *
1740 * ## EXAMPLES
1741 *
1742 * # List all publicize connections.
1743 * $ wp jetpack publicize list
1744 *
1745 * # List all publicize connections, ignoring the cache.
1746 * $ wp jetpack publicize list --ignore-cache
1747 *
1748 * # List publicize connections for a given service.
1749 * $ wp jetpack publicize list linkedin
1750 *
1751 * # List all publicize connections for a given user.
1752 * $ wp --user=1 jetpack publicize list
1753 *
1754 * # List all publicize connections for a given user and service.
1755 * $ wp --user=1 jetpack publicize list linkedin
1756 *
1757 * # Display details for a given connection.
1758 * $ wp jetpack publicize list 123456
1759 *
1760 * # Diconnection a given connection.
1761 * $ wp jetpack publicize disconnect 123456
1762 *
1763 * # Disconnect all connections.
1764 * $ wp jetpack publicize disconnect all
1765 *
1766 * # Disconnect all connections for a given service.
1767 * $ wp jetpack publicize disconnect linkedin
1768 *
1769 * @param array $args Positional args.
1770 * @param array $named_args Named args.
1771 */
1772 public function publicize( $args, $named_args ) {
1773 if ( ! Jetpack::connection()->has_connected_owner() ) {
1774 WP_CLI::error( __( 'Jetpack Social requires a user-level connection to WordPress.com', 'jetpack' ) );
1775 }
1776
1777 if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1778 WP_CLI::error( __( 'The Jetpack Social module is not active.', 'jetpack' ) );
1779 }
1780
1781 if ( ( new Status() )->is_offline_mode() ) {
1782 if (
1783 ! defined( 'JETPACK_DEV_DEBUG' ) &&
1784 ! has_filter( 'jetpack_offline_mode' ) &&
1785 ! str_contains( site_url(), '.' )
1786 ) {
1787 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' ) );
1788 }
1789
1790 WP_CLI::error( __( 'Jetpack is currently in offline mode, so the Jetpack Social module will not load.', 'jetpack' ) );
1791 }
1792
1793 if ( ! class_exists( Publicize::class ) ) {
1794 WP_CLI::error( __( 'The Jetpack Social module is not loaded.', 'jetpack' ) );
1795 }
1796
1797 $action = $args[0];
1798 $publicize = new Publicize();
1799 $identifier = ! empty( $args[1] ) ? $args[1] : false;
1800 $services = array_keys( $publicize->get_services() );
1801 $id_is_service = in_array( $identifier, $services, true );
1802
1803 switch ( $action ) {
1804 case 'list':
1805 $_args = array(
1806 'ignore_cache' => $named_args['ignore-cache'] ?? false,
1807 );
1808 // For the CLI command, let's return all connections when a user isn't specified. This
1809 // differs from the logic in the Publicize class.
1810 $connections_to_return = is_user_logged_in()
1811 ? Connections::get_all_for_user( $_args )
1812 : Connections::get_all( $_args );
1813
1814 if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1815 $temp_connections = $connections_to_return;
1816 $connections_to_return = array();
1817
1818 foreach ( $temp_connections as $connection ) {
1819 if ( $identifier === $connection['service_name'] ) {
1820 $connections_to_return[] = $connection;
1821 }
1822 }
1823 }
1824
1825 if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1826 $connections_to_return = wp_list_filter( $connections_to_return, array( 'connection_id' => $identifier ) );
1827 }
1828
1829 $expected_keys = array(
1830 'connection_id',
1831 'service_name',
1832 'display_name',
1833 'external_id',
1834 'wpcom_user_id',
1835 'shared',
1836 );
1837
1838 // Somehow, a test site ended up in a state where $connections_to_return looked like:
1839 // array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1840 // This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1841 // to minimize future issues, this nested loop will remove any connections that don't contain
1842 // any keys that we expect.
1843 foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1844 foreach ( $expected_keys as $expected_key ) {
1845 if ( ! isset( $connection[ $expected_key ] ) ) {
1846 unset( $connections_to_return[ $connection_key ] );
1847 continue;
1848 }
1849 }
1850 }
1851
1852 if ( empty( $connections_to_return ) ) {
1853 return false;
1854 }
1855
1856 WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1857 break; // list.
1858 case 'disconnect':
1859 if ( ! $identifier ) {
1860 WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1861 }
1862
1863 // If the connection ID is 'all' then delete all connections. If the connection ID
1864 // matches a service, delete all connections for that service.
1865 if ( 'all' === $identifier || $id_is_service ) {
1866 if ( 'all' === $identifier ) {
1867 WP_CLI::log( __( "You're about to delete all Jetpack Social connections.", 'jetpack' ) );
1868 } else {
1869 /* translators: %s is a lowercase string for a social network. */
1870 WP_CLI::log( sprintf( __( "You're about to delete all Jetpack Social connections to %s.", 'jetpack' ), $identifier ) );
1871 }
1872
1873 jetpack_cli_are_you_sure();
1874
1875 $service = $identifier;
1876 $connections = is_user_logged_in()
1877 ? Connections::get_all_for_user()
1878 : Connections::get_all();
1879
1880 if ( 'all' !== $service ) {
1881 $connections = wp_list_filter( $connections, array( 'service_name' => $service ) );
1882 }
1883
1884 if ( ! empty( $connections ) ) {
1885 $count = is_countable( $connections ) ? count( $connections ) : 0;
1886 $progress = \WP_CLI\Utils\make_progress_bar(
1887 /* translators: %s is a lowercase string for a social network. */
1888 sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1889 $count
1890 );
1891
1892 foreach ( $connections as $connection ) {
1893 $id = $connection['connection_id'];
1894 if ( false === $publicize->disconnect( false, $id ) ) {
1895 WP_CLI::error(
1896 sprintf(
1897 /* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1898 __( 'Jetpack Social connection %d could not be disconnected', 'jetpack' ),
1899 $id
1900 )
1901 );
1902 }
1903
1904 // @phan-suppress-next-line PhanUndeclaredClassMethod - Class is missing from php-stubs/wp-cli-stubs 🤷
1905 $progress->tick();
1906 }
1907
1908 // @phan-suppress-next-line PhanUndeclaredClassMethod - Class is missing from php-stubs/wp-cli-stubs 🤷
1909 $progress->finish();
1910
1911 if ( 'all' === $service ) {
1912 WP_CLI::success( __( 'All Jetpack Social connections were successfully disconnected.', 'jetpack' ) );
1913 } else {
1914 /* translators: %s is a lowercase string for a social network. */
1915 WP_CLI::success( sprintf( __( 'All Jetpack Social connections to %s were successfully disconnected.', 'jetpack' ), $service ) );
1916 }
1917 }
1918 } elseif ( false !== $publicize->disconnect( false, $identifier ) ) {
1919 /* translators: %d is a numeric ID. Example: 1234. */
1920 WP_CLI::success( sprintf( __( 'Jetpack Social connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1921 } else {
1922 /* translators: %d is a numeric ID. Example: 1234. */
1923 WP_CLI::error( sprintf( __( 'Jetpack Social connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1924 }
1925 break; // disconnect.
1926 }
1927 }
1928
1929 /**
1930 * Get the API host.
1931 *
1932 * @return string URL.
1933 */
1934 private function get_api_host() {
1935 $env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1936 return $env_api_host ? 'https://' . $env_api_host : JETPACK__WPCOM_JSON_API_BASE;
1937 }
1938
1939 /**
1940 * Log and exit on a partner provision error.
1941 *
1942 * @param WP_Error $error Error.
1943 * @return never
1944 */
1945 private function partner_provision_error( $error ) {
1946 WP_CLI::log(
1947 wp_json_encode(
1948 array(
1949 'success' => false,
1950 'error_code' => $error->get_error_code(),
1951 'error_message' => $error->get_error_message(),
1952 ),
1953 JSON_UNESCAPED_SLASHES
1954 )
1955 );
1956 exit( 1 );
1957 }
1958
1959 /**
1960 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1961 *
1962 * ## TYPES
1963 *
1964 * 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.
1965 *
1966 * ## BLOCK TYPE OPTIONS
1967 *
1968 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1969 * 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.
1970 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1971 * --description: Allows to provide a text description of the block.
1972 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1973 * --variation: Allows to decide whether the block should be a production block, experimental, or beta. Defaults to Beta when arg not provided.
1974 *
1975 * ## BLOCK TYPE EXAMPLES
1976 *
1977 * wp jetpack scaffold block "Cool Block"
1978 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1979 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1980 * wp jetpack scaffold block "Jukebox" --variation="experimental"
1981 *
1982 * @subcommand scaffold block
1983 * @synopsis <type> <title> [--slug] [--description] [--keywords] [--variation]
1984 *
1985 * @param array $args Positional parameters, when strings are passed, wrap them in quotes.
1986 * @param array $assoc_args Associative parameters like --slug="nice-block".
1987 */
1988 public function scaffold( $args, $assoc_args ) {
1989 // It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1990 switch ( $args[0] ) {
1991 case 'block':
1992 $this->block( $args, $assoc_args );
1993 break;
1994 default:
1995 /* translators: %s is the subcommand */
1996 WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1997 exit( 1 );
1998 }
1999 }
2000
2001 /**
2002 * Creates the essential files in Jetpack to build a Gutenberg block.
2003 *
2004 * @param array $args Positional parameters. Only one is used, that corresponds to the block title.
2005 * @param array $assoc_args Associative parameters defined in the scaffold() method.
2006 */
2007 public function block( $args, $assoc_args ) {
2008 if ( ! isset( $args[1] ) ) {
2009 WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
2010 exit( 1 );
2011 }
2012
2013 $title = ucwords( $args[1] );
2014
2015 $slug = isset( $assoc_args['slug'] )
2016 ? $assoc_args['slug']
2017 : sanitize_title( $title );
2018
2019 $next_version = "\x24\x24next-version$$"; // Escapes to hide the string from tools/replace-next-version-tag.sh
2020
2021 $variation_options = array( 'production', 'experimental', 'beta' );
2022 $variation = ( isset( $assoc_args['variation'] ) && in_array( $assoc_args['variation'], $variation_options, true ) )
2023 ? $assoc_args['variation']
2024 : 'beta';
2025
2026 if ( preg_match( '#^jetpack/#', $slug ) ) {
2027 $slug = preg_replace( '#^jetpack/#', '', $slug );
2028 }
2029
2030 if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
2031 WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
2032 }
2033
2034 global $wp_filesystem;
2035 if ( ! WP_Filesystem() ) {
2036 WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
2037 }
2038
2039 $path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
2040
2041 if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
2042 /* translators: %s is path to the conflicting block */
2043 WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
2044 exit( 1 );
2045 }
2046
2047 $wp_filesystem->mkdir( $path );
2048
2049 $keywords = isset( $assoc_args['keywords'] )
2050 ? array_map(
2051 function ( $keyword ) {
2052 return trim( $keyword );
2053 },
2054 array_slice( explode( ',', $assoc_args['keywords'] ), 0, 3 )
2055 )
2056 : array();
2057
2058 $files = array(
2059 "$path/block.json" => self::render_block_file(
2060 'block-block-json',
2061 array(
2062 'slug' => $slug,
2063 'title' => wp_json_encode( $title, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
2064 'description' => isset( $assoc_args['description'] )
2065 ? wp_json_encode( $assoc_args['description'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
2066 : wp_json_encode( $title, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
2067 'nextVersion' => $next_version,
2068 'keywords' => wp_json_encode( $keywords, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
2069 )
2070 ),
2071 "$path/$slug.php" => self::render_block_file(
2072 'block-register-php',
2073 array(
2074 'nextVersion' => $next_version,
2075 'title' => $title,
2076 'underscoredTitle' => str_replace( ' ', '_', $title ),
2077 )
2078 ),
2079 "$path/editor.js" => self::render_block_file( 'block-editor-js' ),
2080 "$path/editor.scss" => self::render_block_file(
2081 'block-editor-scss',
2082 array(
2083 'slug' => $slug,
2084 'title' => $title,
2085 )
2086 ),
2087 "$path/edit.js" => self::render_block_file(
2088 'block-edit-js',
2089 array(
2090 'title' => $title,
2091 'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
2092 )
2093 ),
2094 );
2095
2096 $files_written = array();
2097
2098 foreach ( $files as $filename => $contents ) {
2099 if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
2100 $files_written[] = $filename;
2101 } else {
2102 /* translators: %s is a file name */
2103 WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
2104 }
2105 }
2106
2107 if ( empty( $files_written ) ) {
2108 WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
2109 } else {
2110 // Load index.json and insert the slug of the new block in its block variation array.
2111 $block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
2112 $block_list = $wp_filesystem->get_contents( $block_list_path );
2113 if ( empty( $block_list ) ) {
2114 /* translators: %s is the path to the file with the block list */
2115 WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
2116 } elseif ( false === stripos( $block_list, $slug ) ) {
2117 $new_block_list = json_decode( $block_list );
2118 $new_block_list->{ $variation }[] = $slug;
2119
2120 // Format the JSON to match our coding standards.
2121 $new_block_list_formatted = wp_json_encode( $new_block_list, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) . "\n";
2122 $new_block_list_formatted = preg_replace_callback(
2123 // Find all occurrences of multiples of 4 spaces a the start of the line.
2124 '/^((?: )+)/m',
2125 function ( $matches ) {
2126 // Replace each occurrence of 4 spaces with a tab character.
2127 return str_repeat( "\t", substr_count( $matches[0], ' ' ) );
2128 },
2129 $new_block_list_formatted
2130 );
2131
2132 if ( ! $wp_filesystem->put_contents( $block_list_path, $new_block_list_formatted ) ) {
2133 /* translators: %s is the path to the file with the block list */
2134 WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
2135 }
2136 }
2137
2138 if ( 'beta' === $variation || 'experimental' === $variation ) {
2139 $block_constant = sprintf(
2140 /* translators: the placeholder is a constant name */
2141 esc_html__( 'To load the block, add the constant JETPACK_BLOCKS_VARIATION set to %1$s to your wp-config.php file', 'jetpack' ),
2142 $variation
2143 );
2144 } else {
2145 $block_constant = '';
2146 }
2147
2148 WP_CLI::success(
2149 sprintf(
2150 /* translators: the placeholders are a human readable title, and a series of words separated by dashes */
2151 esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
2152 "--------------------------------------------------------------------------------------------------------------------\n" .
2153 /* translators: the placeholder is a directory path */
2154 esc_html__( 'The files were created at %3$s', 'jetpack' ) . "\n" .
2155 esc_html__( 'To start using the block, build the blocks with pnpm run build-extensions', 'jetpack' ) . "\n" .
2156 /* translators: the placeholder is a file path */
2157 esc_html__( 'The block slug has been added to the %4$s list at %5$s', 'jetpack' ) . "\n" .
2158 '%6$s' . "\n" .
2159 /* translators: the placeholder is a URL */
2160 "\n" . esc_html__( 'Read more at %7$s', 'jetpack' ) . "\n",
2161 $title,
2162 $slug,
2163 $path,
2164 $variation,
2165 $block_list_path,
2166 $block_constant,
2167 'https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/extensions/README.md#developing-block-editor-extensions-in-jetpack'
2168 ) . '--------------------------------------------------------------------------------------------------------------------'
2169 );
2170 }
2171 }
2172
2173 /**
2174 * Built the file replacing the placeholders in the template with the data supplied.
2175 *
2176 * @param string $template Template.
2177 * @param array $data Data.
2178 * @return string mixed
2179 */
2180 private static function render_block_file( $template, $data = array() ) {
2181 return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
2182 }
2183 }
2184
2185 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
2186
2187 /**
2188 * Standard "ask for permission to continue" function.
2189 * If action cancelled, ask if they need help.
2190 *
2191 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
2192 *
2193 * @param bool $flagged false = normal option | true = flagged by get_jetpack_options_for_reset().
2194 * @param string $error_msg Error message.
2195 */
2196 function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
2197 $cli = new Jetpack_CLI();
2198
2199 // Default cancellation message.
2200 if ( ! $error_msg ) {
2201 $error_msg =
2202 __( 'Action cancelled. Have a question?', 'jetpack' )
2203 . ' '
2204 . $cli->green_open
2205 . 'jetpack.com/support'
2206 . $cli->color_close;
2207 }
2208
2209 if ( ! $flagged ) {
2210 $prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
2211 } else {
2212 $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' );
2213 }
2214
2215 WP_CLI::line( $prompt_message );
2216 $handle = fopen( 'php://stdin', 'r' );
2217 $line = fgets( $handle );
2218 if ( 'yes' !== trim( $line ) ) {
2219 WP_CLI::error( $error_msg );
2220 }
2221 }
2222