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