PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-rest-connector.php

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

1,378 lines 41.9 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 disconnect Jetpack.
748 *
749 * @since 1.15.0
750 *
751 * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
752 */
753 public static function jetpack_reconnect_permission_check() {
754 if ( current_user_can( 'jetpack_reconnect' ) ) {
755 return true;
756 }
757
758 return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
759 }
760
761 /**
762 * Returns generic error message when user is not allowed to perform an action.
763 *
764 * @return string The error message.
765 */
766 public static function get_user_permissions_error_msg() {
767 return self::$user_permissions_error_msg;
768 }
769
770 /**
771 * The endpoint tried to partially or fully reconnect the website to WP.com.
772 *
773 * @since 1.15.0
774 *
775 * @return \WP_REST_Response|WP_Error
776 */
777 public function connection_reconnect() {
778 $response = array();
779
780 $next = null;
781
782 $result = $this->connection->restore();
783
784 if ( is_wp_error( $result ) ) {
785 $response = $result;
786 } elseif ( is_string( $result ) ) {
787 $next = $result;
788 } else {
789 $next = true === $result ? 'completed' : 'failed';
790 }
791
792 switch ( $next ) {
793 case 'authorize':
794 $response['status'] = 'in_progress';
795 $response['authorizeUrl'] = $this->connection->get_authorization_url();
796 break;
797 case 'completed':
798 $response['status'] = 'completed';
799 /**
800 * Action fired when reconnection has completed successfully.
801 *
802 * @since 1.18.1
803 */
804 do_action( 'jetpack_reconnection_completed' );
805 break;
806 case 'failed':
807 $response = new WP_Error( 'Reconnect failed' );
808 break;
809 }
810
811 return rest_ensure_response( $response );
812 }
813
814 /**
815 * Verify that user is allowed to connect Jetpack.
816 *
817 * @since 1.26.0
818 *
819 * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
820 */
821 public static function jetpack_register_permission_check() {
822 if ( current_user_can( 'jetpack_connect' ) ) {
823 return true;
824 }
825
826 return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
827 }
828
829 /**
830 * The endpoint tried to connect Jetpack site to WPCOM.
831 *
832 * @since 1.7.0
833 * @since 6.7.0 No longer needs `registration_nonce`.
834 * @since-jetpack 7.7.0
835 *
836 * @param \WP_REST_Request $request The request sent to the WP REST API.
837 *
838 * @return \WP_REST_Response|WP_Error
839 */
840 public function connection_register( $request ) {
841 $from = isset( $request['from'] ) ? (string) $request['from'] : '';
842 if ( '' !== $from ) {
843 $this->connection->add_register_request_param( 'from', $from );
844 }
845
846 if ( ! empty( $request['plugin_slug'] ) ) {
847 // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
848 $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
849 if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
850 $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
851 }
852 }
853
854 $result = $this->connection->try_registration();
855
856 if ( is_wp_error( $result ) ) {
857 return $result;
858 }
859
860 $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
861
862 $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri, '' !== $from ? $from : false );
863
864 /**
865 * Filters the response of jetpack/v4/connection/register endpoint
866 *
867 * @param array $response Array response
868 * @since 1.27.0
869 */
870 $response_body = apply_filters(
871 'jetpack_register_site_rest_response',
872 array()
873 );
874
875 // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
876 $response_body['authorizeUrl'] = $authorize_url;
877 if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
878 $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
879 }
880
881 return rest_ensure_response( $response_body );
882 }
883
884 /**
885 * Get the authorization URL.
886 *
887 * @since 1.27.0
888 *
889 * @param \WP_REST_Request $request The request sent to the WP REST API.
890 *
891 * @return \WP_REST_Response|WP_Error
892 */
893 public function connection_authorize_url( $request ) {
894 $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
895 $from = $request->get_param( 'from' );
896 $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri, ! empty( $from ) ? (string) $from : false );
897
898 return rest_ensure_response(
899 array(
900 'authorizeUrl' => $authorize_url,
901 )
902 );
903 }
904
905 /**
906 * The endpoint tried to partially or fully reconnect the website to WP.com.
907 *
908 * @since 1.29.0
909 *
910 * @param \WP_REST_Request $request The request sent to the WP REST API.
911 *
912 * @return \WP_REST_Response|WP_Error
913 */
914 public static function update_user_token( $request ) {
915 $token_parts = explode( '.', $request['user_token'] );
916
917 if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
918 return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
919 }
920
921 $user_id = (int) $token_parts[2];
922
923 if ( false === get_userdata( $user_id ) ) {
924 return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
925 }
926
927 $connection = new Manager();
928
929 if ( ! $connection->is_connected() ) {
930 return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
931 }
932
933 $is_connection_owner = isset( $request['is_connection_owner'] )
934 ? (bool) $request['is_connection_owner']
935 : ( new Manager() )->get_connection_owner_id() === $user_id;
936
937 ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
938
939 /**
940 * Fires when the user token gets successfully replaced.
941 *
942 * @since 1.29.0
943 *
944 * @param int $user_id User ID.
945 * @param string $token New user token.
946 */
947 do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );
948
949 return rest_ensure_response(
950 array(
951 'success' => true,
952 )
953 );
954 }
955
956 /**
957 * Disconnects Jetpack from the WordPress.com Servers
958 *
959 * @since 1.30.1
960 *
961 * @return bool|WP_Error True if Jetpack successfully disconnected.
962 */
963 public static function disconnect_site() {
964 $connection = new Manager();
965
966 if ( $connection->is_connected() ) {
967 $connection->disconnect_site();
968 return rest_ensure_response( array( 'code' => 'success' ) );
969 }
970
971 return new WP_Error(
972 'disconnect_failed',
973 esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
974 array( 'status' => 400 )
975 );
976 }
977
978 /**
979 * Unlinks current user from the WordPress.com Servers.
980 *
981 * @since 6.3.3
982 *
983 * @param WP_REST_Request $request The request sent to the WP REST API.
984 *
985 * @return bool|WP_Error True if user successfully unlinked.
986 */
987 public static function unlink_user( $request ) {
988
989 if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
990 return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
991 }
992
993 // 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.
994 $disconnect_all_users = false;
995
996 if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
997 if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
998 $disconnect_all_users = true;
999 } else {
1000 return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
1001 }
1002 }
1003
1004 // Allow admins to force a disconnect by passing the "force" parameter
1005 // This allows an admin to disconnect themselves
1006 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 ) ) {
1007 return rest_ensure_response(
1008 array(
1009 'code' => 'success',
1010 )
1011 );
1012 } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
1013 return rest_ensure_response(
1014 array(
1015 'code' => 'success',
1016 )
1017 );
1018 }
1019
1020 return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
1021 }
1022
1023 /**
1024 * Verify that the API client is allowed to replace user token.
1025 *
1026 * @since 1.29.0
1027 *
1028 * @return bool|WP_Error
1029 */
1030 public static function update_user_token_permission_check() {
1031 return Rest_Authentication::is_signed_with_blog_token()
1032 ? true
1033 : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1034 }
1035
1036 /**
1037 * Change the connection owner.
1038 *
1039 * @since 1.29.0
1040 *
1041 * @param WP_REST_Request $request The request sent to the WP REST API.
1042 *
1043 * @return \WP_REST_Response|WP_Error
1044 */
1045 public static function set_connection_owner( $request ) {
1046 $new_owner_id = $request['owner'];
1047
1048 $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );
1049
1050 if ( is_wp_error( $owner_set ) ) {
1051 return $owner_set;
1052 }
1053
1054 return rest_ensure_response(
1055 array(
1056 'code' => 'success',
1057 )
1058 );
1059 }
1060
1061 /**
1062 * Check that user has permission to change the master user.
1063 *
1064 * @since 1.7.0
1065 * @since-jetpack 6.2.0
1066 * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
1067 *
1068 * @return bool|WP_Error True if user is able to change master user.
1069 */
1070 public static function set_connection_owner_permission_check() {
1071 if ( current_user_can( 'jetpack_disconnect' ) ) {
1072 return true;
1073 }
1074
1075 return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1076 }
1077
1078 /**
1079 * The endpoint verifies blog connection and blog token validity.
1080 *
1081 * @since 2.7.0
1082 *
1083 * @return mixed|null
1084 */
1085 public function connection_check() {
1086 /**
1087 * Filters the successful response of the REST API test_connection method
1088 *
1089 * @param string $response The response string.
1090 */
1091 $status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
1092
1093 return rest_ensure_response(
1094 array(
1095 'status' => $status,
1096 )
1097 );
1098 }
1099
1100 /**
1101 * Remote connect endpoint permission check.
1102 *
1103 * @return true|WP_Error
1104 */
1105 public function connection_check_permission_check() {
1106 if ( current_user_can( 'jetpack_connect' ) ) {
1107 return true;
1108 }
1109
1110 return Rest_Authentication::is_signed_with_blog_token()
1111 ? true
1112 : new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1113 }
1114
1115 /**
1116 * Permission check for the connection/test endpoint.
1117 *
1118 * @since 8.5.0
1119 *
1120 * @return true|WP_Error
1121 */
1122 public static function connection_test_permission_check() {
1123 if ( current_user_can( 'manage_options' ) ) {
1124 return true;
1125 }
1126
1127 return new WP_Error(
1128 'invalid_user_permission_manage_options',
1129 self::get_user_permissions_error_msg(),
1130 array( 'status' => rest_authorization_required_code() )
1131 );
1132 }
1133
1134 /**
1135 * Whether the current user may read the site record.
1136 *
1137 * The floor is `edit_posts` because the Jetpack dashboard requests this route on mount and
1138 * is reachable by contributors, matching how My Jetpack and admin-ui gate their pages.
1139 *
1140 * An offline site keeps its blog ID and blog token, so the fetch stays signed and reaches
1141 * WordPress.com. The floor there is `manage_options`, matching the capability the route
1142 * carried before it moved into this package.
1143 *
1144 * @since 8.10.0
1145 *
1146 * @return true|WP_Error
1147 */
1148 public static function site_data_permission_check() {
1149 if ( ( new Status() )->is_offline_mode() ) {
1150 if ( current_user_can( 'manage_options' ) ) {
1151 return true;
1152 }
1153 } elseif ( current_user_can( 'edit_posts' ) ) {
1154 return true;
1155 }
1156
1157 return new WP_Error(
1158 'invalid_user_permission_view_admin',
1159 self::get_user_permissions_error_msg(),
1160 array( 'status' => rest_authorization_required_code() )
1161 );
1162 }
1163
1164 /**
1165 * Return the site's WordPress.com record, or an error envelope describing the failure.
1166 *
1167 * @since 8.10.0
1168 *
1169 * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1170 */
1171 public function get_site_data() {
1172 return self::site_data_response( $this->connection );
1173 }
1174
1175 /**
1176 * Build the site data response.
1177 *
1178 * Separate from the route callback so callers that only want the response, such as the
1179 * Jetpack plugin's deprecated wrapper, do not have to construct a `REST_Connector` and
1180 * re-register the routes.
1181 *
1182 * @since 8.10.0
1183 *
1184 * @param Manager|null $connection The connection manager to fetch with. Defaults to a new one.
1185 * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1186 */
1187 public static function site_data_response( ?Manager $connection = null ) {
1188 $site_data = ( $connection ?? new Manager() )->get_connected_site_data();
1189
1190 if ( ! is_wp_error( $site_data ) ) {
1191 /**
1192 * Fires when the site data was successfully returned from the /sites/%d wpcom endpoint.
1193 *
1194 * @since 8.10.0
1195 * @since-jetpack 8.7.0
1196 */
1197 do_action( 'jetpack_get_site_data_success' );
1198
1199 return rest_ensure_response(
1200 array(
1201 'code' => 'success',
1202 'message' => esc_html__( 'Site data correctly received.', 'jetpack-connection' ),
1203 'data' => wp_json_encode( $site_data, JSON_UNESCAPED_SLASHES ),
1204 )
1205 );
1206 }
1207
1208 $error_data = $site_data->get_error_data();
1209
1210 if ( empty( $error_data['api_error_code'] ) ) {
1211 $error_message = esc_html__( 'Failed fetching site data from WordPress.com. If the problem persists, try reconnecting Jetpack.', 'jetpack-connection' );
1212 } else {
1213 /* translators: %s is an error code (e.g. `token_mismatch`) */
1214 $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'] );
1215 }
1216
1217 return new WP_Error(
1218 $site_data->get_error_code(),
1219 $error_message,
1220 array(
1221 'status' => 400,
1222 'api_error_code' => empty( $error_data['api_error_code'] ) ? null : $error_data['api_error_code'],
1223 'api_http_code' => empty( $error_data['api_http_code'] ) ? null : $error_data['api_http_code'],
1224 )
1225 );
1226 }
1227
1228 /**
1229 * Run all connection health tests and return the result.
1230 *
1231 * @since 8.5.0
1232 *
1233 * @return WP_REST_Response|WP_Error
1234 */
1235 public function connection_test() {
1236 $cxntests = new Connection_Health_Tests();
1237 $tests_run = array_keys( $cxntests->list_tests() );
1238
1239 if ( $cxntests->pass() ) {
1240 return rest_ensure_response(
1241 array(
1242 'code' => 'success',
1243 'message' => __( 'All connection tests passed.', 'jetpack-connection' ),
1244 'tests_run' => $tests_run,
1245 )
1246 );
1247 }
1248
1249 return $cxntests->output_fails_as_wp_error();
1250 }
1251
1252 /**
1253 * Run connection health tests for a privileged external caller (WP.com debugger).
1254 *
1255 * Results are encrypted so only WP.com can read them.
1256 *
1257 * @since 8.5.0
1258 *
1259 * @return WP_REST_Response
1260 */
1261 public function connection_test_for_external() {
1262 // Since we are running this test for inclusion in the WP.com testing suite,
1263 // let's not try to run them as part of these results.
1264 add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
1265 $cxntests = new Connection_Health_Tests();
1266
1267 if ( $cxntests->pass() ) {
1268 $result = array(
1269 'code' => 'success',
1270 'message' => __( 'All connection tests passed.', 'jetpack-connection' ),
1271 );
1272 } else {
1273 $error = $cxntests->output_fails_as_wp_error();
1274 $errors = array();
1275
1276 // Borrowed from WP_REST_Server::error_to_response().
1277 foreach ( (array) $error->errors as $code => $messages ) {
1278 foreach ( (array) $messages as $message ) {
1279 $errors[] = array(
1280 'code' => $code,
1281 'message' => $message,
1282 'data' => $error->get_error_data( $code ),
1283 );
1284 }
1285 }
1286
1287 $result = ( ! empty( $errors ) ) ? $errors[0] : null;
1288 if ( count( $errors ) > 1 ) {
1289 // Remove the primary error.
1290 array_shift( $errors );
1291 $result['additional_errors'] = $errors;
1292 }
1293 }
1294
1295 $result = wp_json_encode( $result, JSON_UNESCAPED_SLASHES );
1296
1297 $encrypted = $cxntests->encrypt_string_for_wpcom( $result );
1298
1299 if ( ! $encrypted || ! is_array( $encrypted ) ) {
1300 return rest_ensure_response(
1301 array(
1302 'code' => 'action_required',
1303 'message' => 'Please request results from the in-plugin debugger',
1304 )
1305 );
1306 }
1307
1308 return rest_ensure_response(
1309 array(
1310 'code' => 'response',
1311 'debug' => $encrypted,
1312 )
1313 );
1314 }
1315
1316 /**
1317 * Permission check for the connection/data endpoint
1318 *
1319 * @return bool|WP_Error
1320 */
1321 public static function user_connection_data_permission_check() {
1322 if ( current_user_can( 'jetpack_connect_user' ) ) {
1323 return true;
1324 }
1325
1326 return new WP_Error(
1327 'invalid_user_permission_user_connection_data',
1328 self::get_user_permissions_error_msg(),
1329 array( 'status' => rest_authorization_required_code() )
1330 );
1331 }
1332
1333 /**
1334 * Verifies if the request was signed with the Jetpack Debugger key
1335 *
1336 * @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.
1337 *
1338 * @return bool
1339 */
1340 public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
1341 // phpcs:disable WordPress.Security.NonceVerification.Recommended
1342 if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
1343 return false;
1344 }
1345
1346 // signature timestamp must be within 5min of current time.
1347 if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1348 return false;
1349 }
1350
1351 $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1352
1353 $signature_data = wp_json_encode(
1354 array(
1355 'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
1356 'timestamp' => (int) $_GET['timestamp'],
1357 'url' => filter_var( wp_unslash( $_GET['url'] ) ),
1358 ),
1359 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.
1360 );
1361
1362 if (
1363 ! function_exists( 'openssl_verify' )
1364 || 1 !== openssl_verify(
1365 $signature_data,
1366 $signature,
1367 is_string( $pub_key ) ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
1368 )
1369 ) {
1370 return false;
1371 }
1372
1373 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1374
1375 return true;
1376 }
1377 }
1378