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