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
← All changes | jetpack_vendor/automattic/jetpack-connection/src/class-rest-connector.php +1563 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1563 @@
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 +}