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