PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 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-beta, at jetpack_vendor/automattic/jetpack-connection/src/class-rest-connector.php

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