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