PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 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 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-rest-connector.php

class-rest-connector.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-connection/src/class-rest-connector.php

1,385 lines 42.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sets up the Connection REST API endpoints.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8 namespace Automattic\Jetpack\Connection;
9
10 use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
11 use Automattic\Jetpack\Constants;
12 use Automattic\Jetpack\Redirect;
13 use Automattic\Jetpack\Status;
14 use Jetpack_XMLRPC_Server;
15 use WP_Error;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_REST_Server;
19
20 /**
21 * Registers the REST routes for Connections.
22 *
23 * @phan-constructor-used-for-side-effects
24 */
25 class REST_Connector {
26 /**
27 * The Connection Manager.
28 *
29 * @var Manager
30 */
31 private $connection;
32
33 /**
34 * This property stores the localized "Insufficient Permissions" error message.
35 *
36 * @var string Generic error message when user is not allowed to perform an action.
37 */
38 private static $user_permissions_error_msg;
39
40 const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
41 . 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
42 . '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
43 . '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
44 . 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
45 . 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
46 . 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
47 . 'EwIDAQAB' . "\r\n"
48 . '-----END PUBLIC KEY-----' . "\r\n";
49
50 /**
51 * Constructor.
52 *
53 * @param Manager $connection The Connection Manager.
54 */
55 public function __construct( Manager $connection ) {
56 $this->connection = $connection;
57
58 self::$user_permissions_error_msg = esc_html__(
59 'You do not have the correct user permissions to perform this action.
60 Please contact your site admin if you think this is a mistake.',
61 'jetpack-connection'
62 );
63
64 $jp_version = Constants::get_constant( 'JETPACK__VERSION' );
65
66 if ( ! $this->connection->has_connected_owner() ) {
67 // Register a site.
68 register_rest_route(
69 'jetpack/v4',
70 '/verify_registration',
71 array(
72 'methods' => WP_REST_Server::EDITABLE,
73 'callback' => array( $this, 'verify_registration' ),
74 'permission_callback' => '__return_true',
75 )
76 );
77 }
78
79 // Authorize a remote user.
80 register_rest_route(
81 'jetpack/v4',
82 '/remote_authorize',
83 array(
84 'methods' => WP_REST_Server::EDITABLE,
85 'callback' => __CLASS__ . '::remote_authorize',
86 'permission_callback' => '__return_true',
87 )
88 );
89
90 // Authorize a remote user.
91 register_rest_route(
92 'jetpack/v4',
93 '/remote_provision',
94 array(
95 'methods' => WP_REST_Server::EDITABLE,
96 'callback' => array( $this, 'remote_provision' ),
97 'permission_callback' => array( $this, 'remote_provision_permission_check' ),
98 )
99 );
100
101 register_rest_route(
102 'jetpack/v4',
103 '/remote_register',
104 array(
105 'methods' => WP_REST_Server::EDITABLE,
106 'callback' => array( $this, 'remote_register' ),
107 'permission_callback' => array( $this, 'remote_register_permission_check' ),
108 )
109 );
110
111 // Connect a remote user.
112 register_rest_route(
113 'jetpack/v4',
114 '/remote_connect',
115 array(
116 'methods' => WP_REST_Server::EDITABLE,
117 'callback' => array( $this, 'remote_connect' ),
118 'permission_callback' => array( $this, 'remote_connect_permission_check' ),
119 )
120 );
121
122 // The endpoint verifies blog connection and blog token validity.
123 register_rest_route(
124 'jetpack/v4',
125 '/connection/check',
126 array(
127 'methods' => WP_REST_Server::READABLE,
128 'callback' => array( $this, 'connection_check' ),
129 'permission_callback' => array( $this, 'connection_check_permission_check' ),
130 )
131 );
132
133 // Get the site's own record from WordPress.com.
134 register_rest_route(
135 'jetpack/v4',
136 '/site',
137 array(
138 'methods' => WP_REST_Server::READABLE,
139 'callback' => array( $this, 'get_site_data' ),
140 'permission_callback' => __CLASS__ . '::site_data_permission_check',
141 ),
142 true // override other implementations.
143 );
144
145 // Run all connection health tests.
146 register_rest_route(
147 'jetpack/v4',
148 '/connection/test',
149 array(
150 'methods' => WP_REST_Server::READABLE,
151 'callback' => array( $this, 'connection_test' ),
152 'permission_callback' => __CLASS__ . '::connection_test_permission_check',
153 ),
154 true // override other implementations.
155 );
156
157 // Connection health tests for privileged external callers (WP.com debugger).
158 // Trailing slash matches the old Jetpack plugin registration so the override takes effect.
159 register_rest_route(
160 'jetpack/v4',
161 '/connection/test-wpcom/',
162 array(
163 'methods' => WP_REST_Server::READABLE,
164 'callback' => array( $this, 'connection_test_for_external' ),
165 'permission_callback' => __CLASS__ . '::is_request_signed_by_jetpack_debugger',
166 ),
167 true // override other implementations.
168 );
169
170 // Get current connection status of Jetpack.
171 register_rest_route(
172 'jetpack/v4',
173 '/connection',
174 array(
175 'methods' => WP_REST_Server::READABLE,
176 'callback' => __CLASS__ . '::connection_status',
177 'permission_callback' => '__return_true',
178 )
179 );
180
181 // Disconnect site.
182 register_rest_route(
183 'jetpack/v4',
184 '/connection',
185 array(
186 'methods' => WP_REST_Server::EDITABLE,
187 'callback' => __CLASS__ . '::disconnect_site',
188 'permission_callback' => __CLASS__ . '::disconnect_site_permission_check',
189 'args' => array(
190 'isActive' => array(
191 'description' => __( 'Set to false will trigger the site to disconnect.', 'jetpack-connection' ),
192 'validate_callback' => function ( $value ) {
193 if ( false !== $value ) {
194 return new WP_Error(
195 'rest_invalid_param',
196 __( 'The isActive argument should be set to false.', 'jetpack-connection' ),
197 array( 'status' => 400 )
198 );
199 }
200
201 return true;
202 },
203 'required' => true,
204 ),
205 ),
206 )
207 );
208
209 // Disconnect/unlink user from WordPress.com servers.
210 // this endpoint is set to override the older endpoint that was previously in the Jetpack plugin
211 // Override is here in case an older version of the Jetpack plugin is installed alongside an updated standalone.
212 register_rest_route(
213 'jetpack/v4',
214 '/connection/user',
215 array(
216 'methods' => WP_REST_Server::EDITABLE,
217 'callback' => __CLASS__ . '::unlink_user',
218 'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
219 ),
220 true // override other implementations.
221 );
222
223 // We are only registering this route if Jetpack-the-plugin is not active or it's version is ge 10.0-alpha.
224 // The reason for doing so is to avoid conflicts between the Connection package and
225 // older versions of Jetpack, registering the same route twice.
226 if ( empty( $jp_version ) || version_compare( $jp_version, '10.0-alpha', '>=' ) ) {
227 // Get current user connection data.
228 register_rest_route(
229 'jetpack/v4',
230 '/connection/data',
231 array(
232 'methods' => WP_REST_Server::READABLE,
233 'callback' => __CLASS__ . '::get_user_connection_data',
234 'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
235 )
236 );
237 }
238
239 // Get list of plugins that use the Jetpack connection.
240 register_rest_route(
241 'jetpack/v4',
242 '/connection/plugins',
243 array(
244 'methods' => WP_REST_Server::READABLE,
245 'callback' => array( __CLASS__, 'get_connection_plugins' ),
246 'permission_callback' => __CLASS__ . '::connection_plugins_permission_check',
247 )
248 );
249
250 // Full or partial reconnect in case of connection issues.
251 register_rest_route(
252 'jetpack/v4',
253 '/connection/reconnect',
254 array(
255 'methods' => WP_REST_Server::EDITABLE,
256 'callback' => array( $this, 'connection_reconnect' ),
257 'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check',
258 )
259 );
260
261 // Register the site (get `blog_token`).
262 register_rest_route(
263 'jetpack/v4',
264 '/connection/register',
265 array(
266 'methods' => WP_REST_Server::EDITABLE,
267 'callback' => array( $this, 'connection_register' ),
268 'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
269 'args' => array(
270 'from' => array(
271 'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack-connection' ),
272 'type' => 'string',
273 ),
274 'redirect_uri' => array(
275 'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
276 'type' => 'string',
277 ),
278 'plugin_slug' => array(
279 'description' => __( 'Indicates from what plugin the request is coming from', 'jetpack-connection' ),
280 'type' => 'string',
281 ),
282 ),
283 )
284 );
285
286 // Get authorization URL.
287 register_rest_route(
288 'jetpack/v4',
289 '/connection/authorize_url',
290 array(
291 'methods' => WP_REST_Server::READABLE,
292 'callback' => array( $this, 'connection_authorize_url' ),
293 'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
294 'args' => array(
295 'redirect_uri' => array(
296 'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
297 'type' => 'string',
298 ),
299 'from' => array(
300 'description' => __( 'Tracking/segmentation identifier for this authorize URL request', 'jetpack-connection' ),
301 'type' => 'string',
302 ),
303 ),
304 )
305 );
306
307 register_rest_route(
308 'jetpack/v4',
309 '/user-token',
310 array(
311 array(
312 'methods' => WP_REST_Server::EDITABLE,
313 'callback' => array( static::class, 'update_user_token' ),
314 'permission_callback' => array( static::class, 'update_user_token_permission_check' ),
315 'args' => array(
316 'user_token' => array(
317 'description' => __( 'New user token', 'jetpack-connection' ),
318 'type' => 'string',
319 'required' => true,
320 ),
321 'is_connection_owner' => array(
322 'description' => __( 'Is connection owner', 'jetpack-connection' ),
323 'type' => 'boolean',
324 ),
325 ),
326 ),
327 )
328 );
329
330 // Set the connection owner.
331 register_rest_route(
332 'jetpack/v4',
333 '/connection/owner',
334 array(
335 'methods' => WP_REST_Server::EDITABLE,
336 'callback' => array( static::class, 'set_connection_owner' ),
337 'permission_callback' => array( static::class, 'set_connection_owner_permission_check' ),
338 'args' => array(
339 'owner' => array(
340 'description' => __( 'New owner', 'jetpack-connection' ),
341 'type' => 'integer',
342 'required' => true,
343 ),
344 ),
345 )
346 );
347 }
348
349 /**
350 * Handles verification that a site is registered.
351 *
352 * @since 1.7.0
353 * @since-jetpack 5.4.0
354 *
355 * @param WP_REST_Request $request The request sent to the WP REST API.
356 *
357 * @return string|WP_Error
358 */
359 public function verify_registration( WP_REST_Request $request ) {
360 $registration_data = array( $request['secret_1'], $request['state'] );
361
362 return $this->connection->handle_registration( $registration_data );
363 }
364
365 /**
366 * Handles verification that a site is registered
367 *
368 * @since 1.7.0
369 * @since-jetpack 5.4.0
370 *
371 * @param WP_REST_Request $request The request sent to the WP REST API.
372 *
373 * @return array|WP_Error
374 */
375 public static function remote_authorize( $request ) {
376 $xmlrpc_server = new Jetpack_XMLRPC_Server();
377 $result = $xmlrpc_server->remote_authorize( $request );
378
379 if ( is_a( $result, 'IXR_Error' ) ) {
380 $result = new WP_Error( $result->code, $result->message );
381 }
382
383 return $result;
384 }
385
386 /**
387 * Initiate the site provisioning process.
388 *
389 * @since 2.5.0
390 *
391 * @param WP_REST_Request $request The request sent to the WP REST API.
392 *
393 * @return WP_Error|array
394 */
395 public function remote_provision( WP_REST_Request $request ) {
396 $request_data = $request->get_params();
397
398 if ( current_user_can( 'jetpack_connect_user' ) ) {
399 $request_data['local_user'] = get_current_user_id();
400 }
401
402 $xmlrpc_server = new Jetpack_XMLRPC_Server();
403 $result = $xmlrpc_server->remote_provision( $request_data );
404
405 if ( is_a( $result, 'IXR_Error' ) ) {
406 $result = new WP_Error( $result->code, $result->message );
407 }
408
409 return $result;
410 }
411
412 /**
413 * Connect a remote user.
414 *
415 * @since 2.6.0
416 *
417 * @param WP_REST_Request $request The request sent to the WP REST API.
418 *
419 * @return WP_Error|array
420 */
421 public static function remote_connect( WP_REST_Request $request ) {
422 $xmlrpc_server = new Jetpack_XMLRPC_Server();
423 $result = $xmlrpc_server->remote_connect( $request );
424
425 if ( is_a( $result, 'IXR_Error' ) ) {
426 $result = new WP_Error( $result->code, $result->message );
427 }
428
429 return $result;
430 }
431
432 /**
433 * Register the site so that a plan can be provisioned.
434 *
435 * @since 2.5.0
436 *
437 * @param WP_REST_Request $request The request object.
438 *
439 * @return WP_Error|array
440 */
441 public function remote_register( WP_REST_Request $request ) {
442 $xmlrpc_server = new Jetpack_XMLRPC_Server();
443 $result = $xmlrpc_server->remote_register( $request );
444
445 if ( is_a( $result, 'IXR_Error' ) ) {
446 $result = new WP_Error( $result->code, $result->message );
447 }
448
449 return $result;
450 }
451
452 /**
453 * Remote provision endpoint permission check.
454 *
455 * @param WP_REST_Request $request The request object.
456 *
457 * @return true|WP_Error
458 */
459 public function remote_provision_permission_check( WP_REST_Request $request ) {
460 if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
461 return true;
462 }
463
464 return Rest_Authentication::is_signed_with_blog_token()
465 ? true
466 : new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
467 }
468
469 /**
470 * Remote connect endpoint permission check.
471 *
472 * @return true|WP_Error
473 */
474 public function remote_connect_permission_check() {
475 return Rest_Authentication::is_signed_with_blog_token()
476 ? true
477 : new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
478 }
479
480 /**
481 * Remote register endpoint permission check.
482 *
483 * @return true|WP_Error
484 */
485 public function remote_register_permission_check() {
486 if ( $this->connection->has_connected_owner() ) {
487 return Rest_Authentication::is_signed_with_blog_token()
488 ? true
489 : new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
490 }
491
492 return true;
493 }
494
495 /**
496 * Get connection status for this Jetpack site.
497 *
498 * @since 1.7.0
499 * @since-jetpack 4.3.0
500 *
501 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
502 *
503 * @return WP_REST_Response|array Connection information.
504 */
505 public static function connection_status( $rest_response = true ) {
506 $status = new Status();
507 $connection = new Manager();
508
509 $connection_status = array(
510 'isActive' => $connection->has_connected_owner(), // TODO deprecate this.
511 'isStaging' => $status->in_safe_mode(), // TODO deprecate this.
512 'isRegistered' => $connection->is_connected(),
513 'isUserConnected' => $connection->is_user_connected(),
514 'hasConnectedOwner' => $connection->has_connected_owner(),
515 'offlineMode' => array(
516 'isActive' => $status->is_offline_mode(),
517 'constant' => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
518 'url' => $status->is_local_site(),
519 /** This filter is documented in packages/status/src/class-status.php */
520 'filter' => apply_filters( 'jetpack_offline_mode', false ),
521 'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
522 'option' => (bool) get_option( 'jetpack_offline_mode' ),
523 ),
524 'isPublic' => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
525 );
526
527 /**
528 * Filters the connection status data.
529 *
530 * @since 1.25.0
531 *
532 * @param array An array containing the connection status data.
533 */
534 $connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
535
536 if ( $rest_response ) {
537 return rest_ensure_response(
538 $connection_status
539 );
540 } else {
541 return $connection_status;
542 }
543 }
544
545 /**
546 * Get plugins connected to the Jetpack.
547 *
548 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
549 *
550 * @since 1.13.1
551 * @since 1.38.0 Added $rest_response param.
552 *
553 * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
554 */
555 public static function get_connection_plugins( $rest_response = true ) {
556 $plugins = ( new Manager() )->get_connected_plugins();
557
558 if ( is_wp_error( $plugins ) ) {
559 return $plugins;
560 }
561
562 array_walk(
563 $plugins,
564 function ( &$data, $slug ) {
565 $data['slug'] = $slug;
566 }
567 );
568
569 if ( $rest_response ) {
570 return rest_ensure_response( array_values( $plugins ) );
571 }
572
573 return array_values( $plugins );
574 }
575
576 /**
577 * Verify that user can view Jetpack admin page and can activate plugins.
578 *
579 * @since 1.15.0
580 *
581 * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
582 */
583 public static function activate_plugins_permission_check() {
584 if ( current_user_can( 'activate_plugins' ) ) {
585 return true;
586 }
587
588 return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
589 }
590
591 /**
592 * Permission check for the connection_plugins endpoint
593 *
594 * @return bool|WP_Error
595 */
596 public static function connection_plugins_permission_check() {
597 if ( true === static::activate_plugins_permission_check() ) {
598 return true;
599 }
600
601 if ( true === static::is_request_signed_by_jetpack_debugger() ) {
602 return true;
603 }
604
605 return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
606 }
607
608 /**
609 * Permission check for the disconnect site endpoint.
610 *
611 * @since 1.30.1
612 *
613 * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
614 *
615 * @return bool|WP_Error True if user is able to disconnect the site or the request is signed with a blog token (aka a direct request from WPCOM).
616 */
617 public static function disconnect_site_permission_check() {
618 if ( current_user_can( 'jetpack_disconnect' ) ) {
619 return true;
620 }
621
622 return Rest_Authentication::is_signed_with_blog_token()
623 ? true
624 : new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
625 }
626
627 /**
628 * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
629 *
630 * @since 6.3.3
631 *
632 * @return bool|WP_Error True if user is able to unlink.
633 */
634 public static function unlink_user_permission_callback() {
635 // This is a mapped capability
636 // phpcs:ignore WordPress.WP.Capabilities.Unknown
637 if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
638 return true;
639 }
640
641 return new WP_Error(
642 'invalid_user_permission_unlink_user',
643 self::get_user_permissions_error_msg(),
644 array( 'status' => rest_authorization_required_code() )
645 );
646 }
647
648 /**
649 * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
650 * Information about the master/primary user.
651 * Information about the current user.
652 *
653 * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
654 *
655 * @since 1.30.1
656 *
657 * @return \WP_REST_Response|array
658 */
659 public static function get_user_connection_data( $rest_response = true ) {
660 $blog_id = \Jetpack_Options::get_option( 'id' );
661
662 $connection = new Manager();
663
664 $current_user = wp_get_current_user();
665
666 // Token-dependent on purpose: connectionOwner and isMaster describe the
667 // *connected* owner and go null/false when the owner's token is broken. Status
668 // UIs (e.g. My Jetpack's connection card) rely on that meaning. Record-based
669 // ownership identity (who holds the connection per the master_user option,
670 // token or not) is exposed separately via Initial_State's connectionOwner, and
671 // owner token health via connectionStatus.hasConnectedOwner. Do not consolidate
672 // the two derivations: they answer different questions.
673 $connection_owner = $connection->get_connection_owner();
674
675 $owner_display_name = false === $connection_owner ? null : $connection_owner->display_name;
676
677 $is_user_connected = $connection->is_user_connected();
678 $is_master_user = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID );
679 $wpcom_user_data = $connection->get_connected_user_data();
680
681 // Add connected user gravatar to the returned wpcom_user_data.
682 // Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so
683 // clients probably expect that by now.
684 if ( false === $wpcom_user_data ) {
685 $wpcom_user_data = array();
686 }
687 $wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ?
688 get_avatar_url(
689 $wpcom_user_data['email'],
690 array(
691 'size' => 64,
692 'default' => 'mysteryman',
693 )
694 )
695 : false );
696
697 // Check for possible account errors between the local user and WPCOM account.
698 $possible_errors = array();
699 if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) {
700 $user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status();
701 $possible_errors = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] );
702 }
703
704 $current_user_connection_data = array(
705 'isConnected' => $is_user_connected,
706 'isMaster' => $is_master_user,
707 'username' => $current_user->user_login,
708 'id' => $current_user->ID,
709 'blogId' => $blog_id,
710 'wpcomUser' => $wpcom_user_data,
711 'gravatar' => get_avatar_url( $current_user->ID ),
712 'permissions' => array(
713 'connect' => current_user_can( 'jetpack_connect' ),
714 'connect_user' => current_user_can( 'jetpack_connect_user' ),
715 // This is a mapped capability
716 // phpcs:ignore WordPress.WP.Capabilities.Unknown
717 'unlink_user' => current_user_can( 'jetpack_unlink_user' ),
718 'disconnect' => current_user_can( 'jetpack_disconnect' ),
719 'manage_options' => current_user_can( 'manage_options' ),
720 ),
721 'possibleAccountErrors' => $possible_errors,
722 );
723
724 /**
725 * Filters the current user connection data.
726 *
727 * @since 1.30.1
728 *
729 * @param array An array containing the current user connection data.
730 */
731 $current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data );
732
733 $response = array(
734 'currentUser' => $current_user_connection_data,
735 'connectionOwner' => $owner_display_name,
736 'isRegistered' => $connection->is_connected(),
737 );
738
739 if ( $rest_response ) {
740 return rest_ensure_response( $response );
741 }
742
743 return $response;
744 }
745
746 /**
747 * Verify that user is allowed to restore the connection.
748 *
749 * Users with only 'jetpack_connect_user' get through, but connection_reconnect()
750 * limits them to refreshing their own user token.
751 *
752 * @since 1.15.0
753 * @since 9.8.0 Also allows 'jetpack_connect_user'.
754 *
755 * @return bool|WP_Error Whether user has the capability 'jetpack_reconnect' or 'jetpack_connect_user'.
756 */
757 public static function jetpack_reconnect_permission_check() {
758 if ( current_user_can( 'jetpack_reconnect' ) || current_user_can( 'jetpack_connect_user' ) ) {
759 return true;
760 }
761
762 return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
763 }
764
765 /**
766 * Returns generic error message when user is not allowed to perform an action.
767 *
768 * @return string The error message.
769 */
770 public static function get_user_permissions_error_msg() {
771 return self::$user_permissions_error_msg;
772 }
773
774 /**
775 * The endpoint tried to partially or fully reconnect the website to WP.com.
776 *
777 * @since 1.15.0
778 * @since 9.8.0 Users without 'jetpack_reconnect' only refresh their own user token.
779 *
780 * @return \WP_REST_Response|WP_Error
781 */
782 public function connection_reconnect() {
783 $response = array();
784
785 $next = null;
786
787 $result = current_user_can( 'jetpack_reconnect' )
788 ? $this->connection->restore()
789 : $this->connection->refresh_user_token( false );
790
791 if ( is_wp_error( $result ) ) {
792 $response = $result;
793 } elseif ( is_string( $result ) ) {
794 $next = $result;
795 } else {
796 $next = true === $result ? 'completed' : 'failed';
797 }
798
799 switch ( $next ) {
800 case 'authorize':
801 $response['status'] = 'in_progress';
802 $response['authorizeUrl'] = $this->connection->get_authorization_url();
803 break;
804 case 'completed':
805 $response['status'] = 'completed';
806 /**
807 * Action fired when reconnection has completed successfully.
808 *
809 * @since 1.18.1
810 */
811 do_action( 'jetpack_reconnection_completed' );
812 break;
813 case 'failed':
814 $response = new WP_Error( 'Reconnect failed' );
815 break;
816 }
817
818 return rest_ensure_response( $response );
819 }
820
821 /**
822 * Verify that user is allowed to connect Jetpack.
823 *
824 * @since 1.26.0
825 *
826 * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
827 */
828 public static function jetpack_register_permission_check() {
829 if ( current_user_can( 'jetpack_connect' ) ) {
830 return true;
831 }
832
833 return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
834 }
835
836 /**
837 * The endpoint tried to connect Jetpack site to WPCOM.
838 *
839 * @since 1.7.0
840 * @since 6.7.0 No longer needs `registration_nonce`.
841 * @since-jetpack 7.7.0
842 *
843 * @param \WP_REST_Request $request The request sent to the WP REST API.
844 *
845 * @return \WP_REST_Response|WP_Error
846 */
847 public function connection_register( $request ) {
848 $from = isset( $request['from'] ) ? (string) $request['from'] : '';
849 if ( '' !== $from ) {
850 $this->connection->add_register_request_param( 'from', $from );
851 }
852
853 if ( ! empty( $request['plugin_slug'] ) ) {
854 // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
855 $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
856 if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
857 $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
858 }
859 }
860
861 $result = $this->connection->try_registration();
862
863 if ( is_wp_error( $result ) ) {
864 return $result;
865 }
866
867 $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
868
869 $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri, '' !== $from ? $from : false );
870
871 /**
872 * Filters the response of jetpack/v4/connection/register endpoint
873 *
874 * @param array $response Array response
875 * @since 1.27.0
876 */
877 $response_body = apply_filters(
878 'jetpack_register_site_rest_response',
879 array()
880 );
881
882 // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
883 $response_body['authorizeUrl'] = $authorize_url;
884 if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
885 $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
886 }
887
888 return rest_ensure_response( $response_body );
889 }
890
891 /**
892 * Get the authorization URL.
893 *
894 * @since 1.27.0
895 *
896 * @param \WP_REST_Request $request The request sent to the WP REST API.
897 *
898 * @return \WP_REST_Response|WP_Error
899 */
900 public function connection_authorize_url( $request ) {
901 $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
902 $from = $request->get_param( 'from' );
903 $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri, ! empty( $from ) ? (string) $from : false );
904
905 return rest_ensure_response(
906 array(
907 'authorizeUrl' => $authorize_url,
908 )
909 );
910 }
911
912 /**
913 * The endpoint tried to partially or fully reconnect the website to WP.com.
914 *
915 * @since 1.29.0
916 *
917 * @param \WP_REST_Request $request The request sent to the WP REST API.
918 *
919 * @return \WP_REST_Response|WP_Error
920 */
921 public static function update_user_token( $request ) {
922 $token_parts = explode( '.', $request['user_token'] );
923
924 if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
925 return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
926 }
927
928 $user_id = (int) $token_parts[2];
929
930 if ( false === get_userdata( $user_id ) ) {
931 return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
932 }
933
934 $connection = new Manager();
935
936 if ( ! $connection->is_connected() ) {
937 return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
938 }
939
940 $is_connection_owner = isset( $request['is_connection_owner'] )
941 ? (bool) $request['is_connection_owner']
942 : ( new Manager() )->get_connection_owner_id() === $user_id;
943
944 ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
945
946 /**
947 * Fires when the user token gets successfully replaced.
948 *
949 * @since 1.29.0
950 *
951 * @param int $user_id User ID.
952 * @param string $token New user token.
953 */
954 do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );
955
956 return rest_ensure_response(
957 array(
958 'success' => true,
959 )
960 );
961 }
962
963 /**
964 * Disconnects Jetpack from the WordPress.com Servers
965 *
966 * @since 1.30.1
967 *
968 * @return bool|WP_Error True if Jetpack successfully disconnected.
969 */
970 public static function disconnect_site() {
971 $connection = new Manager();
972
973 if ( $connection->is_connected() ) {
974 $connection->disconnect_site();
975 return rest_ensure_response( array( 'code' => 'success' ) );
976 }
977
978 return new WP_Error(
979 'disconnect_failed',
980 esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
981 array( 'status' => 400 )
982 );
983 }
984
985 /**
986 * Unlinks current user from the WordPress.com Servers.
987 *
988 * @since 6.3.3
989 *
990 * @param WP_REST_Request $request The request sent to the WP REST API.
991 *
992 * @return bool|WP_Error True if user successfully unlinked.
993 */
994 public static function unlink_user( $request ) {
995
996 if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
997 return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
998 }
999
1000 // If the user is also connection owner, we need to disconnect all users. Since disconnecting all users is a destructive action, we need to pass a parameter to confirm the action.
1001 $disconnect_all_users = false;
1002
1003 if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
1004 if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
1005 $disconnect_all_users = true;
1006 } else {
1007 return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
1008 }
1009 }
1010
1011 // Allow admins to force a disconnect by passing the "force" parameter
1012 // This allows an admin to disconnect themselves
1013 if ( isset( $request['force'] ) && false !== $request['force'] && current_user_can( 'manage_options' ) && ( new Manager( 'jetpack' ) )->disconnect_user_force( get_current_user_id(), $disconnect_all_users ) ) {
1014 return rest_ensure_response(
1015 array(
1016 'code' => 'success',
1017 )
1018 );
1019 } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
1020 return rest_ensure_response(
1021 array(
1022 'code' => 'success',
1023 )
1024 );
1025 }
1026
1027 return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
1028 }
1029
1030 /**
1031 * Verify that the API client is allowed to replace user token.
1032 *
1033 * @since 1.29.0
1034 *
1035 * @return bool|WP_Error
1036 */
1037 public static function update_user_token_permission_check() {
1038 return Rest_Authentication::is_signed_with_blog_token()
1039 ? true
1040 : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1041 }
1042
1043 /**
1044 * Change the connection owner.
1045 *
1046 * @since 1.29.0
1047 *
1048 * @param WP_REST_Request $request The request sent to the WP REST API.
1049 *
1050 * @return \WP_REST_Response|WP_Error
1051 */
1052 public static function set_connection_owner( $request ) {
1053 $new_owner_id = $request['owner'];
1054
1055 $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );
1056
1057 if ( is_wp_error( $owner_set ) ) {
1058 return $owner_set;
1059 }
1060
1061 return rest_ensure_response(
1062 array(
1063 'code' => 'success',
1064 )
1065 );
1066 }
1067
1068 /**
1069 * Check that user has permission to change the master user.
1070 *
1071 * @since 1.7.0
1072 * @since-jetpack 6.2.0
1073 * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
1074 *
1075 * @return bool|WP_Error True if user is able to change master user.
1076 */
1077 public static function set_connection_owner_permission_check() {
1078 if ( current_user_can( 'jetpack_disconnect' ) ) {
1079 return true;
1080 }
1081
1082 return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1083 }
1084
1085 /**
1086 * The endpoint verifies blog connection and blog token validity.
1087 *
1088 * @since 2.7.0
1089 *
1090 * @return mixed|null
1091 */
1092 public function connection_check() {
1093 /**
1094 * Filters the successful response of the REST API test_connection method
1095 *
1096 * @param string $response The response string.
1097 */
1098 $status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
1099
1100 return rest_ensure_response(
1101 array(
1102 'status' => $status,
1103 )
1104 );
1105 }
1106
1107 /**
1108 * Remote connect endpoint permission check.
1109 *
1110 * @return true|WP_Error
1111 */
1112 public function connection_check_permission_check() {
1113 if ( current_user_can( 'jetpack_connect' ) ) {
1114 return true;
1115 }
1116
1117 return Rest_Authentication::is_signed_with_blog_token()
1118 ? true
1119 : new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1120 }
1121
1122 /**
1123 * Permission check for the connection/test endpoint.
1124 *
1125 * @since 8.5.0
1126 *
1127 * @return true|WP_Error
1128 */
1129 public static function connection_test_permission_check() {
1130 if ( current_user_can( 'manage_options' ) ) {
1131 return true;
1132 }
1133
1134 return new WP_Error(
1135 'invalid_user_permission_manage_options',
1136 self::get_user_permissions_error_msg(),
1137 array( 'status' => rest_authorization_required_code() )
1138 );
1139 }
1140
1141 /**
1142 * Whether the current user may read the site record.
1143 *
1144 * The floor is `edit_posts` because the Jetpack dashboard requests this route on mount and
1145 * is reachable by contributors, matching how My Jetpack and admin-ui gate their pages.
1146 *
1147 * An offline site keeps its blog ID and blog token, so the fetch stays signed and reaches
1148 * WordPress.com. The floor there is `manage_options`, matching the capability the route
1149 * carried before it moved into this package.
1150 *
1151 * @since 8.10.0
1152 *
1153 * @return true|WP_Error
1154 */
1155 public static function site_data_permission_check() {
1156 if ( ( new Status() )->is_offline_mode() ) {
1157 if ( current_user_can( 'manage_options' ) ) {
1158 return true;
1159 }
1160 } elseif ( current_user_can( 'edit_posts' ) ) {
1161 return true;
1162 }
1163
1164 return new WP_Error(
1165 'invalid_user_permission_view_admin',
1166 self::get_user_permissions_error_msg(),
1167 array( 'status' => rest_authorization_required_code() )
1168 );
1169 }
1170
1171 /**
1172 * Return the site's WordPress.com record, or an error envelope describing the failure.
1173 *
1174 * @since 8.10.0
1175 *
1176 * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1177 */
1178 public function get_site_data() {
1179 return self::site_data_response( $this->connection );
1180 }
1181
1182 /**
1183 * Build the site data response.
1184 *
1185 * Separate from the route callback so callers that only want the response, such as the
1186 * Jetpack plugin's deprecated wrapper, do not have to construct a `REST_Connector` and
1187 * re-register the routes.
1188 *
1189 * @since 8.10.0
1190 *
1191 * @param Manager|null $connection The connection manager to fetch with. Defaults to a new one.
1192 * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1193 */
1194 public static function site_data_response( ?Manager $connection = null ) {
1195 $site_data = ( $connection ?? new Manager() )->get_connected_site_data();
1196
1197 if ( ! is_wp_error( $site_data ) ) {
1198 /**
1199 * Fires when the site data was successfully returned from the /sites/%d wpcom endpoint.
1200 *
1201 * @since 8.10.0
1202 * @since-jetpack 8.7.0
1203 */
1204 do_action( 'jetpack_get_site_data_success' );
1205
1206 return rest_ensure_response(
1207 array(
1208 'code' => 'success',
1209 'message' => esc_html__( 'Site data correctly received.', 'jetpack-connection' ),
1210 'data' => wp_json_encode( $site_data, JSON_UNESCAPED_SLASHES ),
1211 )
1212 );
1213 }
1214
1215 $error_data = $site_data->get_error_data();
1216
1217 if ( empty( $error_data['api_error_code'] ) ) {
1218 $error_message = esc_html__( 'Failed fetching site data from WordPress.com. If the problem persists, try reconnecting Jetpack.', 'jetpack-connection' );
1219 } else {
1220 /* translators: %s is an error code (e.g. `token_mismatch`) */
1221 $error_message = sprintf( esc_html__( 'Failed fetching site data from WordPress.com (%s). If the problem persists, try reconnecting Jetpack.', 'jetpack-connection' ), $error_data['api_error_code'] );
1222 }
1223
1224 return new WP_Error(
1225 $site_data->get_error_code(),
1226 $error_message,
1227 array(
1228 'status' => 400,
1229 'api_error_code' => empty( $error_data['api_error_code'] ) ? null : $error_data['api_error_code'],
1230 'api_http_code' => empty( $error_data['api_http_code'] ) ? null : $error_data['api_http_code'],
1231 )
1232 );
1233 }
1234
1235 /**
1236 * Run all connection health tests and return the result.
1237 *
1238 * @since 8.5.0
1239 *
1240 * @return WP_REST_Response|WP_Error
1241 */
1242 public function connection_test() {
1243 $cxntests = new Connection_Health_Tests();
1244 $tests_run = array_keys( $cxntests->list_tests() );
1245
1246 if ( $cxntests->pass() ) {
1247 return rest_ensure_response(
1248 array(
1249 'code' => 'success',
1250 'message' => __( 'All connection tests passed.', 'jetpack-connection' ),
1251 'tests_run' => $tests_run,
1252 )
1253 );
1254 }
1255
1256 return $cxntests->output_fails_as_wp_error();
1257 }
1258
1259 /**
1260 * Run connection health tests for a privileged external caller (WP.com debugger).
1261 *
1262 * Results are encrypted so only WP.com can read them.
1263 *
1264 * @since 8.5.0
1265 *
1266 * @return WP_REST_Response
1267 */
1268 public function connection_test_for_external() {
1269 // Since we are running this test for inclusion in the WP.com testing suite,
1270 // let's not try to run them as part of these results.
1271 add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
1272 $cxntests = new Connection_Health_Tests();
1273
1274 if ( $cxntests->pass() ) {
1275 $result = array(
1276 'code' => 'success',
1277 'message' => __( 'All connection tests passed.', 'jetpack-connection' ),
1278 );
1279 } else {
1280 $error = $cxntests->output_fails_as_wp_error();
1281 $errors = array();
1282
1283 // Borrowed from WP_REST_Server::error_to_response().
1284 foreach ( (array) $error->errors as $code => $messages ) {
1285 foreach ( (array) $messages as $message ) {
1286 $errors[] = array(
1287 'code' => $code,
1288 'message' => $message,
1289 'data' => $error->get_error_data( $code ),
1290 );
1291 }
1292 }
1293
1294 $result = ( ! empty( $errors ) ) ? $errors[0] : null;
1295 if ( count( $errors ) > 1 ) {
1296 // Remove the primary error.
1297 array_shift( $errors );
1298 $result['additional_errors'] = $errors;
1299 }
1300 }
1301
1302 $result = wp_json_encode( $result, JSON_UNESCAPED_SLASHES );
1303
1304 $encrypted = $cxntests->encrypt_string_for_wpcom( $result );
1305
1306 if ( ! $encrypted || ! is_array( $encrypted ) ) {
1307 return rest_ensure_response(
1308 array(
1309 'code' => 'action_required',
1310 'message' => 'Please request results from the in-plugin debugger',
1311 )
1312 );
1313 }
1314
1315 return rest_ensure_response(
1316 array(
1317 'code' => 'response',
1318 'debug' => $encrypted,
1319 )
1320 );
1321 }
1322
1323 /**
1324 * Permission check for the connection/data endpoint
1325 *
1326 * @return bool|WP_Error
1327 */
1328 public static function user_connection_data_permission_check() {
1329 if ( current_user_can( 'jetpack_connect_user' ) ) {
1330 return true;
1331 }
1332
1333 return new WP_Error(
1334 'invalid_user_permission_user_connection_data',
1335 self::get_user_permissions_error_msg(),
1336 array( 'status' => rest_authorization_required_code() )
1337 );
1338 }
1339
1340 /**
1341 * Verifies if the request was signed with the Jetpack Debugger key
1342 *
1343 * @param string|null $pub_key The public key used to verify the signature. Default is the Jetpack Debugger key. This is used for testing purposes.
1344 *
1345 * @return bool
1346 */
1347 public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
1348 // phpcs:disable WordPress.Security.NonceVerification.Recommended
1349 if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
1350 return false;
1351 }
1352
1353 // signature timestamp must be within 5min of current time.
1354 if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1355 return false;
1356 }
1357
1358 $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1359
1360 $signature_data = wp_json_encode(
1361 array(
1362 'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
1363 'timestamp' => (int) $_GET['timestamp'],
1364 'url' => filter_var( wp_unslash( $_GET['url'] ) ),
1365 ),
1366 0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
1367 );
1368
1369 if (
1370 ! function_exists( 'openssl_verify' )
1371 || 1 !== openssl_verify(
1372 $signature_data,
1373 $signature,
1374 is_string( $pub_key ) ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
1375 )
1376 ) {
1377 return false;
1378 }
1379
1380 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1381
1382 return true;
1383 }
1384 }
1385