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