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