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