PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
jetpack / jetpack_vendor / automattic / jetpack-connection / src / class-rest-connector.php

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

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