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