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