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