PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.6.1
Jetpack – WP Security, Backup, Speed, & Growth v14.6.1
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 1 year ago _inc 1 day ago css 1 year ago extensions 1 year ago images 1 year ago jetpack_vendor 1 day ago json-endpoints 1 year ago modules 1 year ago sal 1 year ago src 1 year ago vendor 1 year ago views 2 years ago CHANGELOG.md 1 year ago LICENSE.txt 5 years ago SECURITY.md 2 years ago class-jetpack-connection-status.php 2 years ago class-jetpack-gallery-settings.php 3 years ago class-jetpack-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 1 year ago class.jetpack-affiliate.php 2 years ago class.jetpack-autoupdate.php 2 years ago class.jetpack-bbpress-json-api.compat.php 2 years ago class.jetpack-cli.php 1 year ago class.jetpack-client-server.php 2 years ago class.jetpack-gutenberg.php 1 year ago class.jetpack-heartbeat.php 2 years ago class.jetpack-modules-list-table.php 2 years ago class.jetpack-network-sites-list-table.php 2 years ago class.jetpack-network.php 1 year ago class.jetpack-plan.php 3 years ago class.jetpack-post-images.php 1 year ago class.jetpack-twitter-cards.php 2 years ago class.jetpack-user-agent.php 2 years ago class.jetpack.php 1 year ago class.json-api-endpoints.php 1 year ago class.json-api.php 1 year ago class.photon.php 3 years ago composer.json 1 year ago enhanced-open-graph.php 3 years 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 global.d.ts 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 4 years 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
2173 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 *
1315 * ## EXAMPLES
1316 *
1317 * wp jetpack sitemap rebuild
1318 *
1319 * @subcommand sitemap
1320 * @synopsis <rebuild> [--purge]
1321 *
1322 * @param array $args Positional args.
1323 * @param array $assoc_args Named args.
1324 */
1325 public function sitemap( $args, $assoc_args ) {
1326 if ( ! Jetpack::is_module_active( 'sitemaps' ) ) {
1327 WP_CLI::error( __( 'Jetpack Sitemaps module is not currently active. Activate it first if you want to work with sitemaps.', 'jetpack' ) );
1328 }
1329 if ( ! class_exists( 'Jetpack_Sitemap_Builder' ) ) {
1330 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' ) );
1331 }
1332
1333 if ( isset( $assoc_args['purge'] ) && $assoc_args['purge'] ) {
1334 $librarian = new Jetpack_Sitemap_Librarian();
1335 $librarian->delete_all_stored_sitemap_data();
1336
1337 // Clear sitemap-related transients
1338 delete_transient( 'jetpack_news_sitemap_xml' );
1339 delete_transient( 'jetpack-sitemap-state-lock' );
1340 WP_CLI::success( 'Purged all sitemap data and cleared sitemap transients.' );
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 * [--ignore-cache]
1676 * : Whether to ignore connections cache.
1677 *
1678 * [--format=<format>]
1679 * : Allows overriding the output of the command when listing connections.
1680 * ---
1681 * default: table
1682 * options:
1683 * - table
1684 * - json
1685 * - csv
1686 * - yaml
1687 * - ids
1688 * - count
1689 * ---
1690 *
1691 * ## EXAMPLES
1692 *
1693 * # List all publicize connections.
1694 * $ wp jetpack publicize list
1695 *
1696 * # List all publicize connections, ignoring the cache.
1697 * $ wp jetpack publicize list --ignore-cache
1698 *
1699 * # List publicize connections for a given service.
1700 * $ wp jetpack publicize list linkedin
1701 *
1702 * # List all publicize connections for a given user.
1703 * $ wp --user=1 jetpack publicize list
1704 *
1705 * # List all publicize connections for a given user and service.
1706 * $ wp --user=1 jetpack publicize list linkedin
1707 *
1708 * # Display details for a given connection.
1709 * $ wp jetpack publicize list 123456
1710 *
1711 * # Diconnection a given connection.
1712 * $ wp jetpack publicize disconnect 123456
1713 *
1714 * # Disconnect all connections.
1715 * $ wp jetpack publicize disconnect all
1716 *
1717 * # Disconnect all connections for a given service.
1718 * $ wp jetpack publicize disconnect linkedin
1719 *
1720 * @param array $args Positional args.
1721 * @param array $named_args Named args.
1722 */
1723 public function publicize( $args, $named_args ) {
1724 if ( ! Jetpack::connection()->has_connected_owner() ) {
1725 WP_CLI::error( __( 'Jetpack Social requires a user-level connection to WordPress.com', 'jetpack' ) );
1726 }
1727
1728 if ( ! Jetpack::is_module_active( 'publicize' ) ) {
1729 WP_CLI::error( __( 'The Jetpack Social module is not active.', 'jetpack' ) );
1730 }
1731
1732 if ( ( new Status() )->is_offline_mode() ) {
1733 if (
1734 ! defined( 'JETPACK_DEV_DEBUG' ) &&
1735 ! has_filter( 'jetpack_development_mode' ) &&
1736 ! has_filter( 'jetpack_offline_mode' ) &&
1737 ! str_contains( site_url(), '.' )
1738 ) {
1739 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' ) );
1740 }
1741
1742 WP_CLI::error( __( 'Jetpack is currently in offline mode, so the Jetpack Social module will not load.', 'jetpack' ) );
1743 }
1744
1745 if ( ! class_exists( Publicize::class ) ) {
1746 WP_CLI::error( __( 'The Jetpack Social module is not loaded.', 'jetpack' ) );
1747 }
1748
1749 $action = $args[0];
1750 $publicize = new Publicize();
1751 $identifier = ! empty( $args[1] ) ? $args[1] : false;
1752 $services = array_keys( $publicize->get_services() );
1753 $id_is_service = in_array( $identifier, $services, true );
1754
1755 switch ( $action ) {
1756 case 'list':
1757 $_args = array(
1758 'ignore_cache' => $named_args['ignore-cache'] ?? false,
1759 );
1760 // For the CLI command, let's return all connections when a user isn't specified. This
1761 // differs from the logic in the Publicize class.
1762 $connections_to_return = is_user_logged_in()
1763 ? Connections::get_all_for_user( $_args )
1764 : Connections::get_all( $_args );
1765
1766 if ( $id_is_service && ! empty( $identifier ) && ! empty( $connections_to_return ) ) {
1767 $temp_connections = $connections_to_return;
1768 $connections_to_return = array();
1769
1770 foreach ( $temp_connections as $connection ) {
1771 if ( $identifier === $connection['service_name'] ) {
1772 $connections_to_return[] = $connection;
1773 }
1774 }
1775 }
1776
1777 if ( $identifier && ! $id_is_service && ! empty( $connections_to_return ) ) {
1778 $connections_to_return = wp_list_filter( $connections_to_return, array( 'connection_id' => $identifier ) );
1779 }
1780
1781 $expected_keys = array(
1782 'connection_id',
1783 'service_name',
1784 'display_name',
1785 'external_id',
1786 'wpcom_user_id',
1787 'shared',
1788 );
1789
1790 // Somehow, a test site ended up in a state where $connections_to_return looked like:
1791 // array( array( array( 'id' => 0, 'service' => 0 ) ) ) // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
1792 // This caused the CLI command to error when running WP_CLI\Utils\format_items() below. So
1793 // to minimize future issues, this nested loop will remove any connections that don't contain
1794 // any keys that we expect.
1795 foreach ( (array) $connections_to_return as $connection_key => $connection ) {
1796 foreach ( $expected_keys as $expected_key ) {
1797 if ( ! isset( $connection[ $expected_key ] ) ) {
1798 unset( $connections_to_return[ $connection_key ] );
1799 continue;
1800 }
1801 }
1802 }
1803
1804 if ( empty( $connections_to_return ) ) {
1805 return false;
1806 }
1807
1808 WP_CLI\Utils\format_items( $named_args['format'], $connections_to_return, $expected_keys );
1809 break; // list.
1810 case 'disconnect':
1811 if ( ! $identifier ) {
1812 WP_CLI::error( __( 'A connection ID must be passed in order to disconnect.', 'jetpack' ) );
1813 }
1814
1815 // If the connection ID is 'all' then delete all connections. If the connection ID
1816 // matches a service, delete all connections for that service.
1817 if ( 'all' === $identifier || $id_is_service ) {
1818 if ( 'all' === $identifier ) {
1819 WP_CLI::log( __( "You're about to delete all Jetpack Social connections.", 'jetpack' ) );
1820 } else {
1821 /* translators: %s is a lowercase string for a social network. */
1822 WP_CLI::log( sprintf( __( "You're about to delete all Jetpack Social connections to %s.", 'jetpack' ), $identifier ) );
1823 }
1824
1825 jetpack_cli_are_you_sure();
1826
1827 $service = $identifier;
1828 $connections = is_user_logged_in()
1829 ? Connections::get_all_for_user()
1830 : Connections::get_all();
1831
1832 if ( 'all' !== $service ) {
1833 $connections = wp_list_filter( $connections, array( 'service_name' => $service ) );
1834 }
1835
1836 if ( ! empty( $connections ) ) {
1837 $count = is_countable( $connections ) ? count( $connections ) : 0;
1838 $progress = \WP_CLI\Utils\make_progress_bar(
1839 /* translators: %s is a lowercase string for a social network. */
1840 sprintf( __( 'Disconnecting all connections to %s.', 'jetpack' ), $service ),
1841 $count
1842 );
1843
1844 foreach ( $connections as $connection ) {
1845 $id = $connection['connection_id'];
1846 if ( false === $publicize->disconnect( false, $id ) ) {
1847 WP_CLI::error(
1848 sprintf(
1849 /* translators: %1$d is a numeric ID and %2$s is a lowercase string for a social network. */
1850 __( 'Jetpack Social connection %d could not be disconnected', 'jetpack' ),
1851 $id
1852 )
1853 );
1854 }
1855
1856 // @phan-suppress-next-line PhanUndeclaredClassMethod - Class is missing from php-stubs/wp-cli-stubs 🤷
1857 $progress->tick();
1858 }
1859
1860 // @phan-suppress-next-line PhanUndeclaredClassMethod - Class is missing from php-stubs/wp-cli-stubs 🤷
1861 $progress->finish();
1862
1863 if ( 'all' === $service ) {
1864 WP_CLI::success( __( 'All Jetpack Social connections were successfully disconnected.', 'jetpack' ) );
1865 } else {
1866 /* translators: %s is a lowercase string for a social network. */
1867 WP_CLI::success( sprintf( __( 'All Jetpack Social connections to %s were successfully disconnected.', 'jetpack' ), $service ) );
1868 }
1869 }
1870 } elseif ( false !== $publicize->disconnect( false, $identifier ) ) {
1871 /* translators: %d is a numeric ID. Example: 1234. */
1872 WP_CLI::success( sprintf( __( 'Jetpack Social connection %d has been disconnected.', 'jetpack' ), $identifier ) );
1873 } else {
1874 /* translators: %d is a numeric ID. Example: 1234. */
1875 WP_CLI::error( sprintf( __( 'Jetpack Social connection %d could not be disconnected.', 'jetpack' ), $identifier ) );
1876 }
1877 break; // disconnect.
1878 }
1879 }
1880
1881 /**
1882 * Get the API host.
1883 *
1884 * @return string URL.
1885 */
1886 private function get_api_host() {
1887 $env_api_host = getenv( 'JETPACK_START_API_HOST', true );
1888 return $env_api_host ? 'https://' . $env_api_host : JETPACK__WPCOM_JSON_API_BASE;
1889 }
1890
1891 /**
1892 * Log and exit on a partner provision error.
1893 *
1894 * @param WP_Error $error Error.
1895 * @return never
1896 */
1897 private function partner_provision_error( $error ) {
1898 WP_CLI::log(
1899 wp_json_encode(
1900 array(
1901 'success' => false,
1902 'error_code' => $error->get_error_code(),
1903 'error_message' => $error->get_error_message(),
1904 )
1905 )
1906 );
1907 exit( 1 );
1908 }
1909
1910 /**
1911 * Creates the essential files in Jetpack to start building a Gutenberg block or plugin.
1912 *
1913 * ## TYPES
1914 *
1915 * 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.
1916 *
1917 * ## BLOCK TYPE OPTIONS
1918 *
1919 * The first parameter is the block title and it's not associative. Add it wrapped in quotes.
1920 * 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.
1921 * --slug: Specific slug to identify the block that overrides the one generated based on the title.
1922 * --description: Allows to provide a text description of the block.
1923 * --keywords: Provide up to three keywords separated by comma so users can find this block when they search in Gutenberg's inserter.
1924 * --variation: Allows to decide whether the block should be a production block, experimental, or beta. Defaults to Beta when arg not provided.
1925 *
1926 * ## BLOCK TYPE EXAMPLES
1927 *
1928 * wp jetpack scaffold block "Cool Block"
1929 * wp jetpack scaffold block "Amazing Rock" --slug="good-music" --description="Rock the best music on your site"
1930 * wp jetpack scaffold block "Jukebox" --keywords="music, audio, media"
1931 * wp jetpack scaffold block "Jukebox" --variation="experimental"
1932 *
1933 * @subcommand scaffold block
1934 * @synopsis <type> <title> [--slug] [--description] [--keywords] [--variation]
1935 *
1936 * @param array $args Positional parameters, when strings are passed, wrap them in quotes.
1937 * @param array $assoc_args Associative parameters like --slug="nice-block".
1938 */
1939 public function scaffold( $args, $assoc_args ) {
1940 // It's ok not to check if it's set, because otherwise WPCLI exits earlier.
1941 switch ( $args[0] ) {
1942 case 'block':
1943 $this->block( $args, $assoc_args );
1944 break;
1945 default:
1946 /* translators: %s is the subcommand */
1947 WP_CLI::error( sprintf( esc_html__( 'Invalid subcommand %s.', 'jetpack' ), $args[0] ) . ' 👻' );
1948 exit( 1 );
1949 }
1950 }
1951
1952 /**
1953 * Creates the essential files in Jetpack to build a Gutenberg block.
1954 *
1955 * @param array $args Positional parameters. Only one is used, that corresponds to the block title.
1956 * @param array $assoc_args Associative parameters defined in the scaffold() method.
1957 */
1958 public function block( $args, $assoc_args ) {
1959 if ( ! isset( $args[1] ) ) {
1960 WP_CLI::error( esc_html__( 'The title parameter is required.', 'jetpack' ) . ' 👻' );
1961 exit( 1 );
1962 }
1963
1964 $title = ucwords( $args[1] );
1965
1966 $slug = isset( $assoc_args['slug'] )
1967 ? $assoc_args['slug']
1968 : sanitize_title( $title );
1969
1970 $next_version = "\x24\x24next-version$$"; // Escapes to hide the string from tools/replace-next-version-tag.sh
1971
1972 $variation_options = array( 'production', 'experimental', 'beta' );
1973 $variation = ( isset( $assoc_args['variation'] ) && in_array( $assoc_args['variation'], $variation_options, true ) )
1974 ? $assoc_args['variation']
1975 : 'beta';
1976
1977 if ( preg_match( '#^jetpack/#', $slug ) ) {
1978 $slug = preg_replace( '#^jetpack/#', '', $slug );
1979 }
1980
1981 if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) {
1982 WP_CLI::error( esc_html__( 'Invalid block slug. They can contain only lowercase alphanumeric characters or dashes, and start with a letter', 'jetpack' ) . ' 👻' );
1983 }
1984
1985 global $wp_filesystem;
1986 if ( ! WP_Filesystem() ) {
1987 WP_CLI::error( esc_html__( "Can't write files", 'jetpack' ) . ' 😱' );
1988 }
1989
1990 $path = JETPACK__PLUGIN_DIR . "extensions/blocks/$slug";
1991
1992 if ( $wp_filesystem->exists( $path ) && $wp_filesystem->is_dir( $path ) ) {
1993 /* translators: %s is path to the conflicting block */
1994 WP_CLI::error( sprintf( esc_html__( 'Name conflicts with the existing block %s', 'jetpack' ), $path ) . ' ⛔️' );
1995 exit( 1 );
1996 }
1997
1998 $wp_filesystem->mkdir( $path );
1999
2000 $keywords = isset( $assoc_args['keywords'] )
2001 ? array_map(
2002 function ( $keyword ) {
2003 return trim( $keyword );
2004 },
2005 array_slice( explode( ',', $assoc_args['keywords'] ), 0, 3 )
2006 )
2007 : array();
2008
2009 $files = array(
2010 "$path/block.json" => self::render_block_file(
2011 'block-block-json',
2012 array(
2013 'slug' => $slug,
2014 'title' => wp_json_encode( $title, JSON_UNESCAPED_UNICODE ),
2015 'description' => isset( $assoc_args['description'] )
2016 ? wp_json_encode( $assoc_args['description'], JSON_UNESCAPED_UNICODE )
2017 : wp_json_encode( $title, JSON_UNESCAPED_UNICODE ),
2018 'nextVersion' => $next_version,
2019 'keywords' => wp_json_encode( $keywords, JSON_UNESCAPED_UNICODE ),
2020 )
2021 ),
2022 "$path/$slug.php" => self::render_block_file(
2023 'block-register-php',
2024 array(
2025 'nextVersion' => $next_version,
2026 'title' => $title,
2027 'underscoredTitle' => str_replace( ' ', '_', $title ),
2028 )
2029 ),
2030 "$path/editor.js" => self::render_block_file( 'block-editor-js' ),
2031 "$path/editor.scss" => self::render_block_file(
2032 'block-editor-scss',
2033 array(
2034 'slug' => $slug,
2035 'title' => $title,
2036 )
2037 ),
2038 "$path/edit.js" => self::render_block_file(
2039 'block-edit-js',
2040 array(
2041 'title' => $title,
2042 'className' => str_replace( ' ', '', ucwords( str_replace( '-', ' ', $slug ) ) ),
2043 )
2044 ),
2045 );
2046
2047 $files_written = array();
2048
2049 foreach ( $files as $filename => $contents ) {
2050 if ( $wp_filesystem->put_contents( $filename, $contents ) ) {
2051 $files_written[] = $filename;
2052 } else {
2053 /* translators: %s is a file name */
2054 WP_CLI::error( sprintf( esc_html__( 'Error creating %s', 'jetpack' ), $filename ) );
2055 }
2056 }
2057
2058 if ( empty( $files_written ) ) {
2059 WP_CLI::log( esc_html__( 'No files were created', 'jetpack' ) );
2060 } else {
2061 // Load index.json and insert the slug of the new block in its block variation array.
2062 $block_list_path = JETPACK__PLUGIN_DIR . 'extensions/index.json';
2063 $block_list = $wp_filesystem->get_contents( $block_list_path );
2064 if ( empty( $block_list ) ) {
2065 /* translators: %s is the path to the file with the block list */
2066 WP_CLI::error( sprintf( esc_html__( 'Error fetching contents of %s', 'jetpack' ), $block_list_path ) );
2067 } elseif ( false === stripos( $block_list, $slug ) ) {
2068 $new_block_list = json_decode( $block_list );
2069 $new_block_list->{ $variation }[] = $slug;
2070
2071 // Format the JSON to match our coding standards.
2072 $new_block_list_formatted = wp_json_encode( $new_block_list, JSON_PRETTY_PRINT ) . "\n";
2073 $new_block_list_formatted = preg_replace_callback(
2074 // Find all occurrences of multiples of 4 spaces a the start of the line.
2075 '/^((?: )+)/m',
2076 function ( $matches ) {
2077 // Replace each occurrence of 4 spaces with a tab character.
2078 return str_repeat( "\t", substr_count( $matches[0], ' ' ) );
2079 },
2080 $new_block_list_formatted
2081 );
2082
2083 if ( ! $wp_filesystem->put_contents( $block_list_path, $new_block_list_formatted ) ) {
2084 /* translators: %s is the path to the file with the block list */
2085 WP_CLI::error( sprintf( esc_html__( 'Error writing new %s', 'jetpack' ), $block_list_path ) );
2086 }
2087 }
2088
2089 if ( 'beta' === $variation || 'experimental' === $variation ) {
2090 $block_constant = sprintf(
2091 /* translators: the placeholder is a constant name */
2092 esc_html__( 'To load the block, add the constant JETPACK_BLOCKS_VARIATION set to %1$s to your wp-config.php file', 'jetpack' ),
2093 $variation
2094 );
2095 } else {
2096 $block_constant = '';
2097 }
2098
2099 WP_CLI::success(
2100 sprintf(
2101 /* translators: the placeholders are a human readable title, and a series of words separated by dashes */
2102 esc_html__( 'Successfully created block %1$s with slug %2$s', 'jetpack' ) . ' 🎉' . "\n" .
2103 "--------------------------------------------------------------------------------------------------------------------\n" .
2104 /* translators: the placeholder is a directory path */
2105 esc_html__( 'The files were created at %3$s', 'jetpack' ) . "\n" .
2106 esc_html__( 'To start using the block, build the blocks with pnpm run build-extensions', 'jetpack' ) . "\n" .
2107 /* translators: the placeholder is a file path */
2108 esc_html__( 'The block slug has been added to the %4$s list at %5$s', 'jetpack' ) . "\n" .
2109 '%6$s' . "\n" .
2110 /* translators: the placeholder is a URL */
2111 "\n" . esc_html__( 'Read more at %7$s', 'jetpack' ) . "\n",
2112 $title,
2113 $slug,
2114 $path,
2115 $variation,
2116 $block_list_path,
2117 $block_constant,
2118 'https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/extensions/README.md#developing-block-editor-extensions-in-jetpack'
2119 ) . '--------------------------------------------------------------------------------------------------------------------'
2120 );
2121 }
2122 }
2123
2124 /**
2125 * Built the file replacing the placeholders in the template with the data supplied.
2126 *
2127 * @param string $template Template.
2128 * @param array $data Data.
2129 * @return string mixed
2130 */
2131 private static function render_block_file( $template, $data = array() ) {
2132 return \WP_CLI\Utils\mustache_render( JETPACK__PLUGIN_DIR . "wp-cli-templates/$template.mustache", $data );
2133 }
2134 }
2135
2136 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move these functions to some other file.
2137
2138 /**
2139 * Standard "ask for permission to continue" function.
2140 * If action cancelled, ask if they need help.
2141 *
2142 * Written outside of the class so it's not listed as an executable command w/ 'wp jetpack'
2143 *
2144 * @param bool $flagged false = normal option | true = flagged by get_jetpack_options_for_reset().
2145 * @param string $error_msg Error message.
2146 */
2147 function jetpack_cli_are_you_sure( $flagged = false, $error_msg = false ) {
2148 $cli = new Jetpack_CLI();
2149
2150 // Default cancellation message.
2151 if ( ! $error_msg ) {
2152 $error_msg =
2153 __( 'Action cancelled. Have a question?', 'jetpack' )
2154 . ' '
2155 . $cli->green_open
2156 . 'jetpack.com/support'
2157 . $cli->color_close;
2158 }
2159
2160 if ( ! $flagged ) {
2161 $prompt_message = _x( 'Are you sure? This cannot be undone. Type "yes" to continue:', '"yes" is a command - do not translate.', 'jetpack' );
2162 } else {
2163 $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' );
2164 }
2165
2166 WP_CLI::line( $prompt_message );
2167 $handle = fopen( 'php://stdin', 'r' );
2168 $line = fgets( $handle );
2169 if ( 'yes' !== trim( $line ) ) {
2170 WP_CLI::error( $error_msg );
2171 }
2172 }
2173