Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.73% covered (success)
93.73%
688 / 734
60.00% covered (warning)
60.00%
27 / 45
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Connector
93.73% covered (success)
93.73%
688 / 734
60.00% covered (warning)
60.00%
27 / 45
177.11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
270 / 270
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
 get_connection_owner_candidates
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 get_connection_owner_candidates_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 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        // Users who could take over the connection, for the owner change above.
363        register_rest_route(
364            'jetpack/v4',
365            '/connection/owner/candidates',
366            array(
367                'methods'             => WP_REST_Server::READABLE,
368                'callback'            => array( static::class, 'get_connection_owner_candidates' ),
369                'permission_callback' => array( static::class, 'get_connection_owner_candidates_permission_check' ),
370            )
371        );
372
373        // Confirm the current user as the protected owner. Not the connection-owner change above.
374        register_rest_route(
375            'jetpack/v4',
376            '/connection/owner/protect',
377            array(
378                'methods'             => WP_REST_Server::EDITABLE,
379                'callback'            => array( static::class, 'protect_connection_owner' ),
380                'permission_callback' => array( static::class, 'protect_connection_owner_permission_check' ),
381            )
382        );
383
384        // Release the protected owner, leaving ownership open to any connected administrator.
385        register_rest_route(
386            'jetpack/v4',
387            '/connection/owner/release',
388            array(
389                'methods'             => WP_REST_Server::EDITABLE,
390                'callback'            => array( static::class, 'release_connection_owner' ),
391                'permission_callback' => array( static::class, 'release_connection_owner_permission_check' ),
392            )
393        );
394    }
395
396    /**
397     * Handles verification that a site is registered.
398     *
399     * @since 1.7.0
400     * @since-jetpack 5.4.0
401     *
402     * @param WP_REST_Request $request The request sent to the WP REST API.
403     *
404     * @return string|WP_Error
405     */
406    public function verify_registration( WP_REST_Request $request ) {
407        $registration_data = array( $request['secret_1'], $request['state'] );
408
409        return $this->connection->handle_registration( $registration_data );
410    }
411
412    /**
413     * Handles verification that a site is registered
414     *
415     * @since 1.7.0
416     * @since-jetpack 5.4.0
417     *
418     * @param WP_REST_Request $request The request sent to the WP REST API.
419     *
420     * @return array|WP_Error
421     */
422    public static function remote_authorize( $request ) {
423        $xmlrpc_server = new Jetpack_XMLRPC_Server();
424        $result        = $xmlrpc_server->remote_authorize( $request );
425
426        if ( is_a( $result, 'IXR_Error' ) ) {
427            $result = new WP_Error( $result->code, $result->message );
428        }
429
430        return $result;
431    }
432
433    /**
434     * Initiate the site provisioning process.
435     *
436     * @since 2.5.0
437     *
438     * @param WP_REST_Request $request The request sent to the WP REST API.
439     *
440     * @return WP_Error|array
441     */
442    public function remote_provision( WP_REST_Request $request ) {
443        $request_data = $request->get_params();
444
445        if ( current_user_can( 'jetpack_connect_user' ) ) {
446            $request_data['local_user'] = get_current_user_id();
447        }
448
449        $xmlrpc_server = new Jetpack_XMLRPC_Server();
450        $result        = $xmlrpc_server->remote_provision( $request_data );
451
452        if ( is_a( $result, 'IXR_Error' ) ) {
453            $result = new WP_Error( $result->code, $result->message );
454        }
455
456        return $result;
457    }
458
459    /**
460     * Connect a remote user.
461     *
462     * @since 2.6.0
463     *
464     * @param WP_REST_Request $request The request sent to the WP REST API.
465     *
466     * @return WP_Error|array
467     */
468    public static function remote_connect( WP_REST_Request $request ) {
469        $xmlrpc_server = new Jetpack_XMLRPC_Server();
470        $result        = $xmlrpc_server->remote_connect( $request );
471
472        if ( is_a( $result, 'IXR_Error' ) ) {
473            $result = new WP_Error( $result->code, $result->message );
474        }
475
476        return $result;
477    }
478
479    /**
480     * Register the site so that a plan can be provisioned.
481     *
482     * @since 2.5.0
483     *
484     * @param WP_REST_Request $request The request object.
485     *
486     * @return WP_Error|array
487     */
488    public function remote_register( WP_REST_Request $request ) {
489        $xmlrpc_server = new Jetpack_XMLRPC_Server();
490        $result        = $xmlrpc_server->remote_register( $request );
491
492        if ( is_a( $result, 'IXR_Error' ) ) {
493            $result = new WP_Error( $result->code, $result->message );
494        }
495
496        return $result;
497    }
498
499    /**
500     * Remote provision endpoint permission check.
501     *
502     * @param WP_REST_Request $request The request object.
503     *
504     * @return true|WP_Error
505     */
506    public function remote_provision_permission_check( WP_REST_Request $request ) {
507        if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
508            return true;
509        }
510
511        return Rest_Authentication::is_signed_with_blog_token()
512            ? true
513            : new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
514    }
515
516    /**
517     * Remote connect endpoint permission check.
518     *
519     * @return true|WP_Error
520     */
521    public function remote_connect_permission_check() {
522        return Rest_Authentication::is_signed_with_blog_token()
523            ? true
524            : new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
525    }
526
527    /**
528     * Remote register endpoint permission check.
529     *
530     * @return true|WP_Error
531     */
532    public function remote_register_permission_check() {
533        if ( $this->connection->has_connected_owner() ) {
534            return Rest_Authentication::is_signed_with_blog_token()
535                ? true
536                : new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
537        }
538
539        return true;
540    }
541
542    /**
543     * Get connection status for this Jetpack site.
544     *
545     * @since 1.7.0
546     * @since-jetpack 4.3.0
547     *
548     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
549     *
550     * @return WP_REST_Response|array Connection information.
551     */
552    public static function connection_status( $rest_response = true ) {
553        $status     = new Status();
554        $connection = new Manager();
555
556        $connection_status = array(
557            'isActive'          => $connection->has_connected_owner(), // TODO deprecate this.
558            'isStaging'         => $status->in_safe_mode(), // TODO deprecate this.
559            'isRegistered'      => $connection->is_connected(),
560            'isUserConnected'   => $connection->is_user_connected(),
561            'hasConnectedOwner' => $connection->has_connected_owner(),
562            'offlineMode'       => array(
563                'isActive'        => $status->is_offline_mode(),
564                'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
565                'url'             => $status->is_local_site(),
566                /** This filter is documented in packages/status/src/class-status.php */
567                'filter'          => apply_filters( 'jetpack_offline_mode', false ),
568                'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
569                'option'          => (bool) get_option( 'jetpack_offline_mode' ),
570            ),
571            'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
572        );
573
574        /**
575         * Filters the connection status data.
576         *
577         * @since 1.25.0
578         *
579         * @param array An array containing the connection status data.
580         */
581        $connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
582
583        if ( $rest_response ) {
584            return rest_ensure_response(
585                $connection_status
586            );
587        } else {
588            return $connection_status;
589        }
590    }
591
592    /**
593     * Get plugins connected to the Jetpack.
594     *
595     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
596     *
597     * @since 1.13.1
598     * @since 1.38.0 Added $rest_response param.
599     *
600     * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
601     */
602    public static function get_connection_plugins( $rest_response = true ) {
603        $plugins = ( new Manager() )->get_connected_plugins();
604
605        if ( is_wp_error( $plugins ) ) {
606            return $plugins;
607        }
608
609        array_walk(
610            $plugins,
611            function ( &$data, $slug ) {
612                $data['slug'] = $slug;
613            }
614        );
615
616        if ( $rest_response ) {
617            return rest_ensure_response( array_values( $plugins ) );
618        }
619
620        return array_values( $plugins );
621    }
622
623    /**
624     * Verify that user can view Jetpack admin page and can activate plugins.
625     *
626     * @since 1.15.0
627     *
628     * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
629     */
630    public static function activate_plugins_permission_check() {
631        if ( current_user_can( 'activate_plugins' ) ) {
632            return true;
633        }
634
635        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
636    }
637
638    /**
639     * Permission check for the connection_plugins endpoint
640     *
641     * @return bool|WP_Error
642     */
643    public static function connection_plugins_permission_check() {
644        if ( true === static::activate_plugins_permission_check() ) {
645            return true;
646        }
647
648        if ( true === static::is_request_signed_by_jetpack_debugger() ) {
649            return true;
650        }
651
652        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
653    }
654
655    /**
656     * Permission check for the disconnect site endpoint.
657     *
658     * @since 1.30.1
659     *
660     * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
661     *
662     * @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).
663     */
664    public static function disconnect_site_permission_check() {
665        if ( current_user_can( 'jetpack_disconnect' ) ) {
666            return true;
667        }
668
669        return Rest_Authentication::is_signed_with_blog_token()
670            ? true
671            : new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
672    }
673
674    /**
675     * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
676     *
677     * @since 6.3.3
678     *
679     * @return bool|WP_Error True if user is able to unlink.
680     */
681    public static function unlink_user_permission_callback() {
682        // This is a mapped capability
683        // phpcs:ignore WordPress.WP.Capabilities.Unknown
684        if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
685            return true;
686        }
687
688        return new WP_Error(
689            'invalid_user_permission_unlink_user',
690            self::get_user_permissions_error_msg(),
691            array( 'status' => rest_authorization_required_code() )
692        );
693    }
694
695    /**
696     * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
697     * Information about the master/primary user.
698     * Information about the current user.
699     *
700     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
701     *
702     * @since 1.30.1
703     *
704     * @return \WP_REST_Response|array
705     */
706    public static function get_user_connection_data( $rest_response = true ) {
707        $blog_id = \Jetpack_Options::get_option( 'id' );
708
709        $connection = new Manager();
710
711        $current_user = wp_get_current_user();
712
713        // Token-dependent on purpose: connectionOwner and isMaster describe the
714        // *connected* owner and go null/false when the owner's token is broken. Status
715        // UIs (e.g. My Jetpack's connection card) rely on that meaning. Record-based
716        // ownership identity (who holds the connection per the master_user option,
717        // token or not) is exposed separately via Initial_State's connectionOwner, and
718        // owner token health via connectionStatus.hasConnectedOwner. Do not consolidate
719        // the two derivations: they answer different questions.
720        $connection_owner = $connection->get_connection_owner();
721
722        $owner_display_name = false === $connection_owner ? null : $connection_owner->display_name;
723
724        $is_user_connected = $connection->is_user_connected();
725        $is_master_user    = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID );
726        $wpcom_user_data   = $connection->get_connected_user_data();
727
728        // Add connected user gravatar to the returned wpcom_user_data.
729        // Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so
730        // clients probably expect that by now.
731        if ( false === $wpcom_user_data ) {
732            $wpcom_user_data = array();
733        }
734        $wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ?
735        get_avatar_url(
736            $wpcom_user_data['email'],
737            array(
738                'size'    => 64,
739                'default' => 'mysteryman',
740            )
741        )
742        : false );
743
744        // Check for possible account errors between the local user and WPCOM account.
745        $possible_errors = array();
746        if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) {
747            $user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status();
748            $possible_errors     = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] );
749        }
750
751        $current_user_connection_data = array(
752            'isConnected'           => $is_user_connected,
753            'isMaster'              => $is_master_user,
754            'username'              => $current_user->user_login,
755            'id'                    => $current_user->ID,
756            'blogId'                => $blog_id,
757            'wpcomUser'             => $wpcom_user_data,
758            'gravatar'              => get_avatar_url( $current_user->ID ),
759            'permissions'           => array(
760                'connect'        => current_user_can( 'jetpack_connect' ),
761                'connect_user'   => current_user_can( 'jetpack_connect_user' ),
762                // This is a mapped capability
763                // phpcs:ignore WordPress.WP.Capabilities.Unknown
764                'unlink_user'    => current_user_can( 'jetpack_unlink_user' ),
765                'disconnect'     => current_user_can( 'jetpack_disconnect' ),
766                'manage_options' => current_user_can( 'manage_options' ),
767            ),
768            'possibleAccountErrors' => $possible_errors,
769        );
770
771        /**
772         * Filters the current user connection data.
773         *
774         * @since 1.30.1
775         *
776         * @param array An array containing the current user connection data.
777         */
778        $current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data );
779
780        $response = array(
781            'currentUser'     => $current_user_connection_data,
782            'connectionOwner' => $owner_display_name,
783            'isRegistered'    => $connection->is_connected(),
784        );
785
786        if ( $rest_response ) {
787            return rest_ensure_response( $response );
788        }
789
790        return $response;
791    }
792
793    /**
794     * Verify that user is allowed to restore the connection.
795     *
796     * Users with only 'jetpack_connect_user' get through, but connection_reconnect()
797     * limits them to refreshing their own user token.
798     *
799     * @since 1.15.0
800     * @since 9.8.0 Also allows 'jetpack_connect_user'.
801     *
802     * @return bool|WP_Error Whether user has the capability 'jetpack_reconnect' or 'jetpack_connect_user'.
803     */
804    public static function jetpack_reconnect_permission_check() {
805        if ( current_user_can( 'jetpack_reconnect' ) || current_user_can( 'jetpack_connect_user' ) ) {
806            return true;
807        }
808
809        return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
810    }
811
812    /**
813     * Returns generic error message when user is not allowed to perform an action.
814     *
815     * @return string The error message.
816     */
817    public static function get_user_permissions_error_msg() {
818        return self::$user_permissions_error_msg;
819    }
820
821    /**
822     * The endpoint tried to partially or fully reconnect the website to WP.com.
823     *
824     * @since 1.15.0
825     * @since 9.8.0 Users without 'jetpack_reconnect' only refresh their own user token.
826     *
827     * @return \WP_REST_Response|WP_Error
828     */
829    public function connection_reconnect() {
830        $response = array();
831
832        $next = null;
833
834        $result = current_user_can( 'jetpack_reconnect' )
835            ? $this->connection->restore()
836            : $this->connection->refresh_user_token( false );
837
838        if ( is_wp_error( $result ) ) {
839            $response = $result;
840        } elseif ( is_string( $result ) ) {
841            $next = $result;
842        } else {
843            $next = true === $result ? 'completed' : 'failed';
844        }
845
846        switch ( $next ) {
847            case 'authorize':
848                $response['status']       = 'in_progress';
849                $response['authorizeUrl'] = $this->connection->get_authorization_url();
850                break;
851            case 'completed':
852                $response['status'] = 'completed';
853                /**
854                 * Action fired when reconnection has completed successfully.
855                 *
856                 * @since 1.18.1
857                 */
858                do_action( 'jetpack_reconnection_completed' );
859                break;
860            case 'failed':
861                $response = new WP_Error( 'Reconnect failed' );
862                break;
863        }
864
865        return rest_ensure_response( $response );
866    }
867
868    /**
869     * Verify that user is allowed to connect Jetpack.
870     *
871     * @since 1.26.0
872     *
873     * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
874     */
875    public static function jetpack_register_permission_check() {
876        if ( current_user_can( 'jetpack_connect' ) ) {
877            return true;
878        }
879
880        return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
881    }
882
883    /**
884     * The endpoint tried to connect Jetpack site to WPCOM.
885     *
886     * @since 1.7.0
887     * @since 6.7.0 No longer needs `registration_nonce`.
888     * @since-jetpack 7.7.0
889     *
890     * @param \WP_REST_Request $request The request sent to the WP REST API.
891     *
892     * @return \WP_REST_Response|WP_Error
893     */
894    public function connection_register( $request ) {
895        $from = isset( $request['from'] ) ? (string) $request['from'] : '';
896        if ( '' !== $from ) {
897            $this->connection->add_register_request_param( 'from', $from );
898        }
899
900        if ( ! empty( $request['plugin_slug'] ) ) {
901            // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
902            $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
903            if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
904                $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
905            }
906        }
907
908        $result = $this->connection->try_registration();
909
910        if ( is_wp_error( $result ) ) {
911            return $result;
912        }
913
914        $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
915
916        $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri, '' !== $from ? $from : false );
917
918        /**
919         * Filters the response of jetpack/v4/connection/register endpoint
920         *
921         * @param array $response Array response
922         * @since 1.27.0
923         */
924        $response_body = apply_filters(
925            'jetpack_register_site_rest_response',
926            array()
927        );
928
929        // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
930        $response_body['authorizeUrl'] = $authorize_url;
931        if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
932            $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
933        }
934
935        return rest_ensure_response( $response_body );
936    }
937
938    /**
939     * Get the authorization URL.
940     *
941     * @since 1.27.0
942     *
943     * @param \WP_REST_Request $request The request sent to the WP REST API.
944     *
945     * @return \WP_REST_Response|WP_Error
946     */
947    public function connection_authorize_url( $request ) {
948        $redirect_uri  = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
949        $from          = $request->get_param( 'from' );
950        $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri, ! empty( $from ) ? (string) $from : false );
951
952        return rest_ensure_response(
953            array(
954                'authorizeUrl' => $authorize_url,
955            )
956        );
957    }
958
959    /**
960     * The endpoint tried to partially or fully reconnect the website to WP.com.
961     *
962     * @since 1.29.0
963     *
964     * @param \WP_REST_Request $request The request sent to the WP REST API.
965     *
966     * @return \WP_REST_Response|WP_Error
967     */
968    public static function update_user_token( $request ) {
969        $token_parts = explode( '.', $request['user_token'] );
970
971        if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
972            return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
973        }
974
975        $user_id = (int) $token_parts[2];
976
977        if ( false === get_userdata( $user_id ) ) {
978            return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
979        }
980
981        $connection = new Manager();
982
983        if ( ! $connection->is_connected() ) {
984            return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
985        }
986
987        $is_connection_owner = isset( $request['is_connection_owner'] )
988            ? (bool) $request['is_connection_owner']
989            : ( new Manager() )->get_connection_owner_id() === $user_id;
990
991        // Tokens::update_user_token() fires jetpack_updated_user_token itself.
992        ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
993
994        return rest_ensure_response(
995            array(
996                'success' => true,
997            )
998        );
999    }
1000
1001    /**
1002     * Disconnects Jetpack from the WordPress.com Servers
1003     *
1004     * @since 1.30.1
1005     *
1006     * @return bool|WP_Error True if Jetpack successfully disconnected.
1007     */
1008    public static function disconnect_site() {
1009        $connection = new Manager();
1010
1011        if ( $connection->is_connected() ) {
1012            $connection->disconnect_site();
1013            return rest_ensure_response( array( 'code' => 'success' ) );
1014        }
1015
1016        return new WP_Error(
1017            'disconnect_failed',
1018            esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
1019            array( 'status' => 400 )
1020        );
1021    }
1022
1023    /**
1024     * Unlinks current user from the WordPress.com Servers.
1025     *
1026     * @since 6.3.3
1027     *
1028     * @param WP_REST_Request $request The request sent to the WP REST API.
1029     *
1030     * @return bool|WP_Error True if user successfully unlinked.
1031     */
1032    public static function unlink_user( $request ) {
1033
1034        if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
1035            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
1036        }
1037
1038        // 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.
1039        $disconnect_all_users = false;
1040
1041        if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
1042            if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
1043                $disconnect_all_users = true;
1044            } else {
1045                return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
1046            }
1047        }
1048
1049        // Allow admins to force a disconnect by passing the "force" parameter
1050        // This allows an admin to disconnect themselves
1051        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 ) ) {
1052            return rest_ensure_response(
1053                array(
1054                    'code' => 'success',
1055                )
1056            );
1057        } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
1058            return rest_ensure_response(
1059                array(
1060                    'code' => 'success',
1061                )
1062            );
1063        }
1064
1065        return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
1066    }
1067
1068    /**
1069     * Verify that the API client is allowed to replace user token.
1070     *
1071     * @since 1.29.0
1072     *
1073     * @return bool|WP_Error
1074     */
1075    public static function update_user_token_permission_check() {
1076        return Rest_Authentication::is_signed_with_blog_token()
1077            ? true
1078            : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1079    }
1080
1081    /**
1082     * Change the connection owner.
1083     *
1084     * @since 1.29.0
1085     *
1086     * @param WP_REST_Request $request The request sent to the WP REST API.
1087     *
1088     * @return \WP_REST_Response|WP_Error
1089     */
1090    public static function set_connection_owner( $request ) {
1091        $new_owner_id = $request['owner'];
1092
1093        $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );
1094
1095        if ( is_wp_error( $owner_set ) ) {
1096            return $owner_set;
1097        }
1098
1099        return rest_ensure_response(
1100            array(
1101                'code' => 'success',
1102            )
1103        );
1104    }
1105
1106    /**
1107     * Check that user has permission to change the master user.
1108     *
1109     * @since 1.7.0
1110     * @since-jetpack 6.2.0
1111     * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
1112     *
1113     * @return bool|WP_Error True if user is able to change master user.
1114     */
1115    public static function set_connection_owner_permission_check() {
1116        if ( current_user_can( 'jetpack_disconnect' ) ) {
1117            return true;
1118        }
1119
1120        return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1121    }
1122
1123    /**
1124     * Users this connection's ownership could be handed to.
1125     *
1126     * Filtered by what `Manager::update_connection_owner()` requires of a new owner, so the
1127     * list cannot offer someone the change would then refuse.
1128     *
1129     * @since $$next-version$$
1130     *
1131     * @return \WP_REST_Response
1132     */
1133    public static function get_connection_owner_candidates() {
1134        $connection = new Manager();
1135        $owner_id   = (int) $connection->get_connection_owner_id();
1136        $admin_cap  = ( new Roles() )->translate_role_to_cap( 'administrator' );
1137        $candidates = array();
1138
1139        if ( ! $admin_cap ) {
1140            return rest_ensure_response( $candidates );
1141        }
1142
1143        foreach ( $connection->get_connected_users( $admin_cap ) as $user ) {
1144            if ( (int) $user->ID === $owner_id ) {
1145                continue;
1146            }
1147
1148            $candidates[] = array(
1149                'id'          => (int) $user->ID,
1150                'login'       => $user->user_login,
1151                'displayName' => $user->display_name,
1152                'email'       => $user->user_email,
1153            );
1154        }
1155
1156        return rest_ensure_response( $candidates );
1157    }
1158
1159    /**
1160     * Whether the current user may list the users ownership could be handed to.
1161     *
1162     * Same capability as changing the owner: seeing the candidates and acting on them go
1163     * together, and the list carries logins and email addresses.
1164     *
1165     * @since $$next-version$$
1166     *
1167     * @return true|WP_Error
1168     */
1169    public static function get_connection_owner_candidates_permission_check() {
1170        if ( current_user_can( 'jetpack_disconnect' ) ) {
1171            return true;
1172        }
1173
1174        return new WP_Error(
1175            'invalid_user_permission_list_connection_owner_candidates',
1176            self::get_user_permissions_error_msg(),
1177            array( 'status' => rest_authorization_required_code() )
1178        );
1179    }
1180
1181    /**
1182     * Confirm the current user as the protected owner.
1183     *
1184     * The claim is always for the signed-in user. A caller cannot name someone else.
1185     *
1186     * @since 9.9.0
1187     *
1188     * @return WP_REST_Response|WP_Error
1189     */
1190    public static function protect_connection_owner() {
1191        $result = ( new Manager() )->set_protected_owner( get_current_user_id() );
1192
1193        if ( is_wp_error( $result ) ) {
1194            return $result;
1195        }
1196
1197        return rest_ensure_response(
1198            array(
1199                'code' => 'success',
1200            )
1201        );
1202    }
1203
1204    /**
1205     * Whether the current user may confirm a protected owner.
1206     *
1207     * A connected administrator qualifies, and only while a consumer is requesting a protected
1208     * owner. Holding the connection owner slot does not matter.
1209     *
1210     * `requires_protected_owner()` is documented as a momentary answer, but it is the only opt-in
1211     * signal there is, so a consumer that surfaces a confirmation must keep answering true for as
1212     * long as it is on screen. One that flips to false between render and submit turns its own
1213     * link into a 403.
1214     *
1215     * @since 9.9.0
1216     *
1217     * @return true|WP_Error
1218     */
1219    public static function protect_connection_owner_permission_check() {
1220        $user_id   = get_current_user_id();
1221        $admin_cap = ( new Roles() )->translate_role_to_cap( 'administrator' );
1222        $manager   = new Manager();
1223
1224        if (
1225            $user_id
1226            && current_user_can( 'jetpack_connect' )
1227            && $admin_cap
1228            && current_user_can( $admin_cap )
1229            && $manager->is_user_connected( $user_id )
1230            && $manager->requires_protected_owner()
1231        ) {
1232            return true;
1233        }
1234
1235        return new WP_Error(
1236            'invalid_user_permission_protect_owner',
1237            self::get_user_permissions_error_msg(),
1238            array( 'status' => rest_authorization_required_code() )
1239        );
1240    }
1241
1242    /**
1243     * Release the protected owner for this site.
1244     *
1245     * @since 9.9.0
1246     *
1247     * @return WP_REST_Response|WP_Error
1248     */
1249    public static function release_connection_owner() {
1250        $result = ( new Manager() )->release_protected_owner();
1251
1252        if ( is_wp_error( $result ) ) {
1253            return $result;
1254        }
1255
1256        return rest_ensure_response(
1257            array(
1258                'code' => 'success',
1259            )
1260        );
1261    }
1262
1263    /**
1264     * Whether the current user may release the protected owner.
1265     *
1266     * Only the confirmed owner qualifies. WordPress.com is asked again before anything is cleared,
1267     * and its answer is the one that decides.
1268     *
1269     * Deliberately not gated on `requires_protected_owner()`, unlike confirming: a consumer that
1270     * has stopped asking must not strand a site holding a lock it can no longer release.
1271     *
1272     * @since 9.9.0
1273     *
1274     * @return true|WP_Error
1275     */
1276    public static function release_connection_owner_permission_check() {
1277        $user_id   = get_current_user_id();
1278        $admin_cap = ( new Roles() )->translate_role_to_cap( 'administrator' );
1279        $manager   = new Manager();
1280
1281        if (
1282            $user_id
1283            && current_user_can( 'jetpack_connect' )
1284            && $admin_cap
1285            && current_user_can( $admin_cap )
1286            && $manager->is_user_connected( $user_id )
1287        ) {
1288            // `RE_EVALUATE` settles that the connection owner matches the anchor, so pinning this
1289            // user to that owner is what makes it their identity. A matching binding would not:
1290            // Premium Content writes the same key directly, so the IDs are not unique site-wide.
1291            $state = $manager->resolve_protected_owner_state();
1292
1293            if ( Manager::PO_STATE_RE_EVALUATE === $state['status'] && $user_id === (int) $manager->get_connection_owner_id() ) {
1294                return true;
1295            }
1296        }
1297
1298        return new WP_Error(
1299            'invalid_user_permission_release_owner',
1300            self::get_user_permissions_error_msg(),
1301            array( 'status' => rest_authorization_required_code() )
1302        );
1303    }
1304
1305    /**
1306     * The endpoint verifies blog connection and blog token validity.
1307     *
1308     * @since 2.7.0
1309     *
1310     * @return mixed|null
1311     */
1312    public function connection_check() {
1313        /**
1314         * Filters the successful response of the REST API test_connection method
1315         *
1316         * @param string $response The response string.
1317         */
1318        $status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
1319
1320        return rest_ensure_response(
1321            array(
1322                'status' => $status,
1323            )
1324        );
1325    }
1326
1327    /**
1328     * Remote connect endpoint permission check.
1329     *
1330     * @return true|WP_Error
1331     */
1332    public function connection_check_permission_check() {
1333        if ( current_user_can( 'jetpack_connect' ) ) {
1334            return true;
1335        }
1336
1337        return Rest_Authentication::is_signed_with_blog_token()
1338            ? true
1339            : new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1340    }
1341
1342    /**
1343     * Permission check for the connection/test endpoint.
1344     *
1345     * @since 8.5.0
1346     *
1347     * @return true|WP_Error
1348     */
1349    public static function connection_test_permission_check() {
1350        if ( current_user_can( 'manage_options' ) ) {
1351            return true;
1352        }
1353
1354        return new WP_Error(
1355            'invalid_user_permission_manage_options',
1356            self::get_user_permissions_error_msg(),
1357            array( 'status' => rest_authorization_required_code() )
1358        );
1359    }
1360
1361    /**
1362     * Whether the current user may read the site record.
1363     *
1364     * The floor is `edit_posts` because the Jetpack dashboard requests this route on mount and
1365     * is reachable by contributors, matching how My Jetpack and admin-ui gate their pages.
1366     *
1367     * An offline site keeps its blog ID and blog token, so the fetch stays signed and reaches
1368     * WordPress.com. The floor there is `manage_options`, matching the capability the route
1369     * carried before it moved into this package.
1370     *
1371     * @since 8.10.0
1372     *
1373     * @return true|WP_Error
1374     */
1375    public static function site_data_permission_check() {
1376        if ( ( new Status() )->is_offline_mode() ) {
1377            if ( current_user_can( 'manage_options' ) ) {
1378                return true;
1379            }
1380        } elseif ( current_user_can( 'edit_posts' ) ) {
1381            return true;
1382        }
1383
1384        return new WP_Error(
1385            'invalid_user_permission_view_admin',
1386            self::get_user_permissions_error_msg(),
1387            array( 'status' => rest_authorization_required_code() )
1388        );
1389    }
1390
1391    /**
1392     * Return the site's WordPress.com record, or an error envelope describing the failure.
1393     *
1394     * @since 8.10.0
1395     *
1396     * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1397     */
1398    public function get_site_data() {
1399        return self::site_data_response( $this->connection );
1400    }
1401
1402    /**
1403     * Build the site data response.
1404     *
1405     * Separate from the route callback so callers that only want the response, such as the
1406     * Jetpack plugin's deprecated wrapper, do not have to construct a `REST_Connector` and
1407     * re-register the routes.
1408     *
1409     * @since 8.10.0
1410     *
1411     * @param Manager|null $connection The connection manager to fetch with. Defaults to a new one.
1412     * @return WP_Error|\WP_HTTP_Response|WP_REST_Response
1413     */
1414    public static function site_data_response( ?Manager $connection = null ) {
1415        $site_data = ( $connection ?? new Manager() )->get_connected_site_data();
1416
1417        if ( ! is_wp_error( $site_data ) ) {
1418            $site_data = self::exclude_site_options( $site_data );
1419
1420            /**
1421             * Fires when the site data was successfully returned from the /sites/%d wpcom endpoint.
1422             *
1423             * @since 8.10.0
1424             * @since-jetpack 8.7.0
1425             */
1426            do_action( 'jetpack_get_site_data_success' );
1427
1428            return rest_ensure_response(
1429                array(
1430                    'code'    => 'success',
1431                    'message' => esc_html__( 'Site data correctly received.', 'jetpack-connection' ),
1432                    'data'    => wp_json_encode( $site_data, JSON_UNESCAPED_SLASHES ),
1433                )
1434            );
1435        }
1436
1437        $error_data = $site_data->get_error_data();
1438
1439        if ( empty( $error_data['api_error_code'] ) ) {
1440            $error_message = esc_html__( 'Failed fetching site data from WordPress.com. If the problem persists, try reconnecting Jetpack.', 'jetpack-connection' );
1441        } else {
1442            /* translators: %s is an error code (e.g. `token_mismatch`) */
1443            $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'] );
1444        }
1445
1446        return new WP_Error(
1447            $site_data->get_error_code(),
1448            $error_message,
1449            array(
1450                'status'         => 400,
1451                'api_error_code' => empty( $error_data['api_error_code'] ) ? null : $error_data['api_error_code'],
1452                'api_http_code'  => empty( $error_data['api_http_code'] ) ? null : $error_data['api_http_code'],
1453            )
1454        );
1455    }
1456
1457    /**
1458     * Removes EXCLUDED_SITE_OPTIONS from the site record before it is served to a REST caller.
1459     *
1460     * Works on a copy: a listener on 'jetpack_site_data_fetched', or any other internal
1461     * consumer holding the record, keeps seeing it whole.
1462     *
1463     * @since 9.9.1
1464     *
1465     * @param object $site_data The decoded site record.
1466     * @return object The record to serve, with EXCLUDED_SITE_OPTIONS removed.
1467     */
1468    private static function exclude_site_options( $site_data ) {
1469        if ( ! is_object( $site_data ) || ! isset( $site_data->options ) || ! is_object( $site_data->options ) ) {
1470            return $site_data;
1471        }
1472
1473        $site_data          = clone $site_data;
1474        $site_data->options = clone $site_data->options;
1475
1476        foreach ( self::EXCLUDED_SITE_OPTIONS as $option ) {
1477            unset( $site_data->options->$option );
1478        }
1479
1480        return $site_data;
1481    }
1482
1483    /**
1484     * Run all connection health tests and return the result.
1485     *
1486     * @since 8.5.0
1487     *
1488     * @return WP_REST_Response|WP_Error
1489     */
1490    public function connection_test() {
1491        $cxntests  = new Connection_Health_Tests();
1492        $tests_run = array_keys( $cxntests->list_tests() );
1493
1494        if ( $cxntests->pass() ) {
1495            return rest_ensure_response(
1496                array(
1497                    'code'      => 'success',
1498                    'message'   => __( 'All connection tests passed.', 'jetpack-connection' ),
1499                    'tests_run' => $tests_run,
1500                )
1501            );
1502        }
1503
1504        return $cxntests->output_fails_as_wp_error();
1505    }
1506
1507    /**
1508     * Run connection health tests for a privileged external caller (WP.com debugger).
1509     *
1510     * Results are encrypted so only WP.com can read them.
1511     *
1512     * @since 8.5.0
1513     *
1514     * @return WP_REST_Response
1515     */
1516    public function connection_test_for_external() {
1517        // Since we are running this test for inclusion in the WP.com testing suite,
1518        // let's not try to run them as part of these results.
1519        add_filter( 'jetpack_debugger_run_self_test', '__return_false' );
1520        $cxntests = new Connection_Health_Tests();
1521
1522        if ( $cxntests->pass() ) {
1523            $result = array(
1524                'code'    => 'success',
1525                'message' => __( 'All connection tests passed.', 'jetpack-connection' ),
1526            );
1527        } else {
1528            $error  = $cxntests->output_fails_as_wp_error();
1529            $errors = array();
1530
1531            // Borrowed from WP_REST_Server::error_to_response().
1532            foreach ( (array) $error->errors as $code => $messages ) {
1533                foreach ( (array) $messages as $message ) {
1534                    $errors[] = array(
1535                        'code'    => $code,
1536                        'message' => $message,
1537                        'data'    => $error->get_error_data( $code ),
1538                    );
1539                }
1540            }
1541
1542            $result = ( ! empty( $errors ) ) ? $errors[0] : null;
1543            if ( count( $errors ) > 1 ) {
1544                // Remove the primary error.
1545                array_shift( $errors );
1546                $result['additional_errors'] = $errors;
1547            }
1548        }
1549
1550        $result = wp_json_encode( $result, JSON_UNESCAPED_SLASHES );
1551
1552        $encrypted = $cxntests->encrypt_string_for_wpcom( $result );
1553
1554        if ( ! $encrypted || ! is_array( $encrypted ) ) {
1555            return rest_ensure_response(
1556                array(
1557                    'code'    => 'action_required',
1558                    'message' => 'Please request results from the in-plugin debugger',
1559                )
1560            );
1561        }
1562
1563        return rest_ensure_response(
1564            array(
1565                'code'  => 'response',
1566                'debug' => $encrypted,
1567            )
1568        );
1569    }
1570
1571    /**
1572     * Permission check for the connection/data endpoint
1573     *
1574     * @return bool|WP_Error
1575     */
1576    public static function user_connection_data_permission_check() {
1577        if ( current_user_can( 'jetpack_connect_user' ) ) {
1578            return true;
1579        }
1580
1581        return new WP_Error(
1582            'invalid_user_permission_user_connection_data',
1583            self::get_user_permissions_error_msg(),
1584            array( 'status' => rest_authorization_required_code() )
1585        );
1586    }
1587
1588    /**
1589     * Verifies if the request was signed with the Jetpack Debugger key
1590     *
1591     * @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.
1592     *
1593     * @return bool
1594     */
1595    public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
1596         // phpcs:disable WordPress.Security.NonceVerification.Recommended
1597        if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
1598            return false;
1599        }
1600
1601        // signature timestamp must be within 5min of current time.
1602        if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1603            return false;
1604        }
1605
1606        $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1607
1608        $signature_data = wp_json_encode(
1609            array(
1610                'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
1611                'timestamp'  => (int) $_GET['timestamp'],
1612                'url'        => filter_var( wp_unslash( $_GET['url'] ) ),
1613            ),
1614            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.
1615        );
1616
1617        if (
1618            ! function_exists( 'openssl_verify' )
1619            || 1 !== openssl_verify(
1620                $signature_data,
1621                $signature,
1622                is_string( $pub_key ) ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
1623            )
1624        ) {
1625            return false;
1626        }
1627
1628        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1629
1630        return true;
1631    }
1632}