Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.69% covered (success)
93.69%
475 / 507
53.12% covered (warning)
53.12%
17 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Connector
93.69% covered (success)
93.69%
475 / 507
53.12% covered (warning)
53.12%
17 / 32
116.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
209 / 209
100.00% covered (success)
100.00%
1 / 1
5
 verify_registration
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 remote_authorize
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 remote_provision
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 remote_connect
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 remote_register
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 remote_provision_permission_check
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 remote_connect_permission_check
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 remote_register_permission_check
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 connection_status
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 get_connection_plugins
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 activate_plugins_permission_check
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 connection_plugins_permission_check
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 disconnect_site_permission_check
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 unlink_user_permission_callback
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 get_user_connection_data
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
8
 jetpack_reconnect_permission_check
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 get_user_permissions_error_msg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connection_reconnect
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
7.05
 jetpack_register_permission_check
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 connection_register
73.68% covered (warning)
73.68%
14 / 19
0.00% covered (danger)
0.00%
0 / 1
9.17
 connection_authorize_url
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 update_user_token
78.95% covered (warning)
78.95%
15 / 19
0.00% covered (danger)
0.00%
0 / 1
7.46
 disconnect_site
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 unlink_user
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
11.41
 update_user_token_permission_check
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 set_connection_owner
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 set_connection_owner_permission_check
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 connection_check
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 connection_check_permission_check
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 user_connection_data_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 is_request_signed_by_jetpack_debugger
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2/**
3 * Sets up the Connection REST API endpoints.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
11use Automattic\Jetpack\Constants;
12use Automattic\Jetpack\Redirect;
13use Automattic\Jetpack\Status;
14use Jetpack_XMLRPC_Server;
15use WP_Error;
16use WP_REST_Request;
17use WP_REST_Response;
18use WP_REST_Server;
19
20/**
21 * Registers the REST routes for Connections.
22 *
23 * @phan-constructor-used-for-side-effects
24 */
25class REST_Connector {
26    /**
27     * The Connection Manager.
28     *
29     * @var Manager
30     */
31    private $connection;
32
33    /**
34     * This property stores the localized "Insufficient Permissions" error message.
35     *
36     * @var string Generic error message when user is not allowed to perform an action.
37     */
38    private static $user_permissions_error_msg;
39
40    const JETPACK__DEBUGGER_PUBLIC_KEY = "\r\n" . '-----BEGIN PUBLIC KEY-----' . "\r\n"
41    . 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm+uLLVoxGCY71LS6KFc6' . "\r\n"
42    . '1UnF6QGBAsi5XF8ty9kR3/voqfOkpW+gRerM2Kyjy6DPCOmzhZj7BFGtxSV2ZoMX' . "\r\n"
43    . '9ZwWxzXhl/Q/6k8jg8BoY1QL6L2K76icXJu80b+RDIqvOfJruaAeBg1Q9NyeYqLY' . "\r\n"
44    . 'lEVzN2vIwcFYl+MrP/g6Bc2co7Jcbli+tpNIxg4Z+Hnhbs7OJ3STQLmEryLpAxQO' . "\r\n"
45    . 'q8cbhQkMx+FyQhxzSwtXYI/ClCUmTnzcKk7SgGvEjoKGAmngILiVuEJ4bm7Q1yok' . "\r\n"
46    . 'xl9+wcfW6JAituNhml9dlHCWnn9D3+j8pxStHihKy2gVMwiFRjLEeD8K/7JVGkb/' . "\r\n"
47    . 'EwIDAQAB' . "\r\n"
48    . '-----END PUBLIC KEY-----' . "\r\n";
49
50    /**
51     * Constructor.
52     *
53     * @param Manager $connection The Connection Manager.
54     */
55    public function __construct( Manager $connection ) {
56        $this->connection = $connection;
57
58        self::$user_permissions_error_msg = esc_html__(
59            'You do not have the correct user permissions to perform this action.
60            Please contact your site admin if you think this is a mistake.',
61            'jetpack-connection'
62        );
63
64        $jp_version = Constants::get_constant( 'JETPACK__VERSION' );
65
66        if ( ! $this->connection->has_connected_owner() ) {
67            // Register a site.
68            register_rest_route(
69                'jetpack/v4',
70                '/verify_registration',
71                array(
72                    'methods'             => WP_REST_Server::EDITABLE,
73                    'callback'            => array( $this, 'verify_registration' ),
74                    'permission_callback' => '__return_true',
75                )
76            );
77        }
78
79        // Authorize a remote user.
80        register_rest_route(
81            'jetpack/v4',
82            '/remote_authorize',
83            array(
84                'methods'             => WP_REST_Server::EDITABLE,
85                'callback'            => __CLASS__ . '::remote_authorize',
86                'permission_callback' => '__return_true',
87            )
88        );
89
90        // Authorize a remote user.
91        register_rest_route(
92            'jetpack/v4',
93            '/remote_provision',
94            array(
95                'methods'             => WP_REST_Server::EDITABLE,
96                'callback'            => array( $this, 'remote_provision' ),
97                'permission_callback' => array( $this, 'remote_provision_permission_check' ),
98            )
99        );
100
101        register_rest_route(
102            'jetpack/v4',
103            '/remote_register',
104            array(
105                'methods'             => WP_REST_Server::EDITABLE,
106                'callback'            => array( $this, 'remote_register' ),
107                'permission_callback' => array( $this, 'remote_register_permission_check' ),
108            )
109        );
110
111        // Connect a remote user.
112        register_rest_route(
113            'jetpack/v4',
114            '/remote_connect',
115            array(
116                'methods'             => WP_REST_Server::EDITABLE,
117                'callback'            => array( $this, 'remote_connect' ),
118                'permission_callback' => array( $this, 'remote_connect_permission_check' ),
119            )
120        );
121
122        // The endpoint verifies blog connection and blog token validity.
123        register_rest_route(
124            'jetpack/v4',
125            '/connection/check',
126            array(
127                'methods'             => WP_REST_Server::READABLE,
128                'callback'            => array( $this, 'connection_check' ),
129                'permission_callback' => array( $this, 'connection_check_permission_check' ),
130            )
131        );
132
133        // Get current connection status of Jetpack.
134        register_rest_route(
135            'jetpack/v4',
136            '/connection',
137            array(
138                'methods'             => WP_REST_Server::READABLE,
139                'callback'            => __CLASS__ . '::connection_status',
140                'permission_callback' => '__return_true',
141            )
142        );
143
144        // Disconnect site.
145        register_rest_route(
146            'jetpack/v4',
147            '/connection',
148            array(
149                'methods'             => WP_REST_Server::EDITABLE,
150                'callback'            => __CLASS__ . '::disconnect_site',
151                'permission_callback' => __CLASS__ . '::disconnect_site_permission_check',
152                'args'                => array(
153                    'isActive' => array(
154                        'description'       => __( 'Set to false will trigger the site to disconnect.', 'jetpack-connection' ),
155                        'validate_callback' => function ( $value ) {
156                            if ( false !== $value ) {
157                                return new WP_Error(
158                                    'rest_invalid_param',
159                                    __( 'The isActive argument should be set to false.', 'jetpack-connection' ),
160                                    array( 'status' => 400 )
161                                );
162                            }
163
164                            return true;
165                        },
166                        'required'          => true,
167                    ),
168                ),
169            )
170        );
171
172        // Disconnect/unlink user from WordPress.com servers.
173        // this endpoint is set to override the older endpoint that was previously in the Jetpack plugin
174        // Override is here in case an older version of the Jetpack plugin is installed alongside an updated standalone.
175        register_rest_route(
176            'jetpack/v4',
177            '/connection/user',
178            array(
179                'methods'             => WP_REST_Server::EDITABLE,
180                'callback'            => __CLASS__ . '::unlink_user',
181                'permission_callback' => __CLASS__ . '::unlink_user_permission_callback',
182            ),
183            true // override other implementations.
184        );
185
186        // We are only registering this route if Jetpack-the-plugin is not active or it's version is ge 10.0-alpha.
187        // The reason for doing so is to avoid conflicts between the Connection package and
188        // older versions of Jetpack, registering the same route twice.
189        if ( empty( $jp_version ) || version_compare( $jp_version, '10.0-alpha', '>=' ) ) {
190            // Get current user connection data.
191            register_rest_route(
192                'jetpack/v4',
193                '/connection/data',
194                array(
195                    'methods'             => WP_REST_Server::READABLE,
196                    'callback'            => __CLASS__ . '::get_user_connection_data',
197                    'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
198                )
199            );
200        }
201
202        // Get list of plugins that use the Jetpack connection.
203        register_rest_route(
204            'jetpack/v4',
205            '/connection/plugins',
206            array(
207                'methods'             => WP_REST_Server::READABLE,
208                'callback'            => array( __CLASS__, 'get_connection_plugins' ),
209                'permission_callback' => __CLASS__ . '::connection_plugins_permission_check',
210            )
211        );
212
213        // Full or partial reconnect in case of connection issues.
214        register_rest_route(
215            'jetpack/v4',
216            '/connection/reconnect',
217            array(
218                'methods'             => WP_REST_Server::EDITABLE,
219                'callback'            => array( $this, 'connection_reconnect' ),
220                'permission_callback' => __CLASS__ . '::jetpack_reconnect_permission_check',
221            )
222        );
223
224        // Register the site (get `blog_token`).
225        register_rest_route(
226            'jetpack/v4',
227            '/connection/register',
228            array(
229                'methods'             => WP_REST_Server::EDITABLE,
230                'callback'            => array( $this, 'connection_register' ),
231                'permission_callback' => __CLASS__ . '::jetpack_register_permission_check',
232                'args'                => array(
233                    'from'         => array(
234                        'description' => __( 'Indicates where the registration action was triggered for tracking/segmentation purposes', 'jetpack-connection' ),
235                        'type'        => 'string',
236                    ),
237                    'redirect_uri' => array(
238                        'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
239                        'type'        => 'string',
240                    ),
241                    'plugin_slug'  => array(
242                        'description' => __( 'Indicates from what plugin the request is coming from', 'jetpack-connection' ),
243                        'type'        => 'string',
244                    ),
245                ),
246            )
247        );
248
249        // Get authorization URL.
250        register_rest_route(
251            'jetpack/v4',
252            '/connection/authorize_url',
253            array(
254                'methods'             => WP_REST_Server::READABLE,
255                'callback'            => array( $this, 'connection_authorize_url' ),
256                'permission_callback' => __CLASS__ . '::user_connection_data_permission_check',
257                'args'                => array(
258                    'redirect_uri' => array(
259                        'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
260                        'type'        => 'string',
261                    ),
262                ),
263            )
264        );
265
266        register_rest_route(
267            'jetpack/v4',
268            '/user-token',
269            array(
270                array(
271                    'methods'             => WP_REST_Server::EDITABLE,
272                    'callback'            => array( static::class, 'update_user_token' ),
273                    'permission_callback' => array( static::class, 'update_user_token_permission_check' ),
274                    'args'                => array(
275                        'user_token'          => array(
276                            'description' => __( 'New user token', 'jetpack-connection' ),
277                            'type'        => 'string',
278                            'required'    => true,
279                        ),
280                        'is_connection_owner' => array(
281                            'description' => __( 'Is connection owner', 'jetpack-connection' ),
282                            'type'        => 'boolean',
283                        ),
284                    ),
285                ),
286            )
287        );
288
289        // Set the connection owner.
290        register_rest_route(
291            'jetpack/v4',
292            '/connection/owner',
293            array(
294                'methods'             => WP_REST_Server::EDITABLE,
295                'callback'            => array( static::class, 'set_connection_owner' ),
296                'permission_callback' => array( static::class, 'set_connection_owner_permission_check' ),
297                'args'                => array(
298                    'owner' => array(
299                        'description' => __( 'New owner', 'jetpack-connection' ),
300                        'type'        => 'integer',
301                        'required'    => true,
302                    ),
303                ),
304            )
305        );
306    }
307
308    /**
309     * Handles verification that a site is registered.
310     *
311     * @since 1.7.0
312     * @since-jetpack 5.4.0
313     *
314     * @param WP_REST_Request $request The request sent to the WP REST API.
315     *
316     * @return string|WP_Error
317     */
318    public function verify_registration( WP_REST_Request $request ) {
319        $registration_data = array( $request['secret_1'], $request['state'] );
320
321        return $this->connection->handle_registration( $registration_data );
322    }
323
324    /**
325     * Handles verification that a site is registered
326     *
327     * @since 1.7.0
328     * @since-jetpack 5.4.0
329     *
330     * @param WP_REST_Request $request The request sent to the WP REST API.
331     *
332     * @return array|WP_Error
333     */
334    public static function remote_authorize( $request ) {
335        $xmlrpc_server = new Jetpack_XMLRPC_Server();
336        $result        = $xmlrpc_server->remote_authorize( $request );
337
338        if ( is_a( $result, 'IXR_Error' ) ) {
339            $result = new WP_Error( $result->code, $result->message );
340        }
341
342        return $result;
343    }
344
345    /**
346     * Initiate the site provisioning process.
347     *
348     * @since 2.5.0
349     *
350     * @param WP_REST_Request $request The request sent to the WP REST API.
351     *
352     * @return WP_Error|array
353     */
354    public function remote_provision( WP_REST_Request $request ) {
355        $request_data = $request->get_params();
356
357        if ( current_user_can( 'jetpack_connect_user' ) ) {
358            $request_data['local_user'] = get_current_user_id();
359        }
360
361        $xmlrpc_server = new Jetpack_XMLRPC_Server();
362        $result        = $xmlrpc_server->remote_provision( $request_data );
363
364        if ( is_a( $result, 'IXR_Error' ) ) {
365            $result = new WP_Error( $result->code, $result->message );
366        }
367
368        return $result;
369    }
370
371    /**
372     * Connect a remote user.
373     *
374     * @since 2.6.0
375     *
376     * @param WP_REST_Request $request The request sent to the WP REST API.
377     *
378     * @return WP_Error|array
379     */
380    public static function remote_connect( WP_REST_Request $request ) {
381        $xmlrpc_server = new Jetpack_XMLRPC_Server();
382        $result        = $xmlrpc_server->remote_connect( $request );
383
384        if ( is_a( $result, 'IXR_Error' ) ) {
385            $result = new WP_Error( $result->code, $result->message );
386        }
387
388        return $result;
389    }
390
391    /**
392     * Register the site so that a plan can be provisioned.
393     *
394     * @since 2.5.0
395     *
396     * @param WP_REST_Request $request The request object.
397     *
398     * @return WP_Error|array
399     */
400    public function remote_register( WP_REST_Request $request ) {
401        $xmlrpc_server = new Jetpack_XMLRPC_Server();
402        $result        = $xmlrpc_server->remote_register( $request );
403
404        if ( is_a( $result, 'IXR_Error' ) ) {
405            $result = new WP_Error( $result->code, $result->message );
406        }
407
408        return $result;
409    }
410
411    /**
412     * Remote provision endpoint permission check.
413     *
414     * @param WP_REST_Request $request The request object.
415     *
416     * @return true|WP_Error
417     */
418    public function remote_provision_permission_check( WP_REST_Request $request ) {
419        if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
420            return true;
421        }
422
423        return Rest_Authentication::is_signed_with_blog_token()
424            ? true
425            : new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
426    }
427
428    /**
429     * Remote connect endpoint permission check.
430     *
431     * @return true|WP_Error
432     */
433    public function remote_connect_permission_check() {
434        return Rest_Authentication::is_signed_with_blog_token()
435            ? true
436            : new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
437    }
438
439    /**
440     * Remote register endpoint permission check.
441     *
442     * @return true|WP_Error
443     */
444    public function remote_register_permission_check() {
445        if ( $this->connection->has_connected_owner() ) {
446            return Rest_Authentication::is_signed_with_blog_token()
447                ? true
448                : new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
449        }
450
451        return true;
452    }
453
454    /**
455     * Get connection status for this Jetpack site.
456     *
457     * @since 1.7.0
458     * @since-jetpack 4.3.0
459     *
460     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
461     *
462     * @return WP_REST_Response|array Connection information.
463     */
464    public static function connection_status( $rest_response = true ) {
465        $status     = new Status();
466        $connection = new Manager();
467
468        $connection_status = array(
469            'isActive'          => $connection->has_connected_owner(), // TODO deprecate this.
470            'isStaging'         => $status->in_safe_mode(), // TODO deprecate this.
471            'isRegistered'      => $connection->is_connected(),
472            'isUserConnected'   => $connection->is_user_connected(),
473            'hasConnectedOwner' => $connection->has_connected_owner(),
474            'offlineMode'       => array(
475                'isActive'        => $status->is_offline_mode(),
476                'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
477                'url'             => $status->is_local_site(),
478                /** This filter is documented in packages/status/src/class-status.php */
479                'filter'          => apply_filters( 'jetpack_offline_mode', false ),
480                'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
481                'option'          => (bool) get_option( 'jetpack_offline_mode' ),
482            ),
483            'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
484        );
485
486        /**
487         * Filters the connection status data.
488         *
489         * @since 1.25.0
490         *
491         * @param array An array containing the connection status data.
492         */
493        $connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
494
495        if ( $rest_response ) {
496            return rest_ensure_response(
497                $connection_status
498            );
499        } else {
500            return $connection_status;
501        }
502    }
503
504    /**
505     * Get plugins connected to the Jetpack.
506     *
507     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
508     *
509     * @since 1.13.1
510     * @since 1.38.0 Added $rest_response param.
511     *
512     * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
513     */
514    public static function get_connection_plugins( $rest_response = true ) {
515        $plugins = ( new Manager() )->get_connected_plugins();
516
517        if ( is_wp_error( $plugins ) ) {
518            return $plugins;
519        }
520
521        array_walk(
522            $plugins,
523            function ( &$data, $slug ) {
524                $data['slug'] = $slug;
525            }
526        );
527
528        if ( $rest_response ) {
529            return rest_ensure_response( array_values( $plugins ) );
530        }
531
532        return array_values( $plugins );
533    }
534
535    /**
536     * Verify that user can view Jetpack admin page and can activate plugins.
537     *
538     * @since 1.15.0
539     *
540     * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
541     */
542    public static function activate_plugins_permission_check() {
543        if ( current_user_can( 'activate_plugins' ) ) {
544            return true;
545        }
546
547        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
548    }
549
550    /**
551     * Permission check for the connection_plugins endpoint
552     *
553     * @return bool|WP_Error
554     */
555    public static function connection_plugins_permission_check() {
556        if ( true === static::activate_plugins_permission_check() ) {
557            return true;
558        }
559
560        if ( true === static::is_request_signed_by_jetpack_debugger() ) {
561            return true;
562        }
563
564        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
565    }
566
567    /**
568     * Permission check for the disconnect site endpoint.
569     *
570     * @since 1.30.1
571     *
572     * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
573     *
574     * @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).
575     */
576    public static function disconnect_site_permission_check() {
577        if ( current_user_can( 'jetpack_disconnect' ) ) {
578            return true;
579        }
580
581        return Rest_Authentication::is_signed_with_blog_token()
582            ? true
583            : new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
584    }
585
586    /**
587     * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
588     *
589     * @since 6.3.3
590     *
591     * @return bool|WP_Error True if user is able to unlink.
592     */
593    public static function unlink_user_permission_callback() {
594        // This is a mapped capability
595        // phpcs:ignore WordPress.WP.Capabilities.Unknown
596        if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
597            return true;
598        }
599
600        return new WP_Error(
601            'invalid_user_permission_unlink_user',
602            self::get_user_permissions_error_msg(),
603            array( 'status' => rest_authorization_required_code() )
604        );
605    }
606
607    /**
608     * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
609     * Information about the master/primary user.
610     * Information about the current user.
611     *
612     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
613     *
614     * @since 1.30.1
615     *
616     * @return \WP_REST_Response|array
617     */
618    public static function get_user_connection_data( $rest_response = true ) {
619        $blog_id = \Jetpack_Options::get_option( 'id' );
620
621        $connection = new Manager();
622
623        $current_user     = wp_get_current_user();
624        $connection_owner = $connection->get_connection_owner();
625
626        $owner_display_name = false === $connection_owner ? null : $connection_owner->display_name;
627
628        $is_user_connected = $connection->is_user_connected();
629        $is_master_user    = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID );
630        $wpcom_user_data   = $connection->get_connected_user_data();
631
632        // Add connected user gravatar to the returned wpcom_user_data.
633        // Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so
634        // clients probably expect that by now.
635        if ( false === $wpcom_user_data ) {
636            $wpcom_user_data = array();
637        }
638        $wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ?
639        get_avatar_url(
640            $wpcom_user_data['email'],
641            array(
642                'size'    => 64,
643                'default' => 'mysteryman',
644            )
645        )
646        : false );
647
648        // Check for possible account errors between the local user and WPCOM account.
649        $possible_errors = array();
650        if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) {
651            $user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status();
652            $possible_errors     = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] );
653        }
654
655        $current_user_connection_data = array(
656            'isConnected'           => $is_user_connected,
657            'isMaster'              => $is_master_user,
658            'username'              => $current_user->user_login,
659            'id'                    => $current_user->ID,
660            'blogId'                => $blog_id,
661            'wpcomUser'             => $wpcom_user_data,
662            'gravatar'              => get_avatar_url( $current_user->ID ),
663            'permissions'           => array(
664                'connect'        => current_user_can( 'jetpack_connect' ),
665                'connect_user'   => current_user_can( 'jetpack_connect_user' ),
666                // This is a mapped capability
667                // phpcs:ignore WordPress.WP.Capabilities.Unknown
668                'unlink_user'    => current_user_can( 'jetpack_unlink_user' ),
669                'disconnect'     => current_user_can( 'jetpack_disconnect' ),
670                'manage_options' => current_user_can( 'manage_options' ),
671            ),
672            'possibleAccountErrors' => $possible_errors,
673        );
674
675        /**
676         * Filters the current user connection data.
677         *
678         * @since 1.30.1
679         *
680         * @param array An array containing the current user connection data.
681         */
682        $current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data );
683
684        $response = array(
685            'currentUser'     => $current_user_connection_data,
686            'connectionOwner' => $owner_display_name,
687            'isRegistered'    => $connection->is_connected(),
688        );
689
690        if ( $rest_response ) {
691            return rest_ensure_response( $response );
692        }
693
694        return $response;
695    }
696
697    /**
698     * Verify that user is allowed to disconnect Jetpack.
699     *
700     * @since 1.15.0
701     *
702     * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
703     */
704    public static function jetpack_reconnect_permission_check() {
705        if ( current_user_can( 'jetpack_reconnect' ) ) {
706            return true;
707        }
708
709        return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
710    }
711
712    /**
713     * Returns generic error message when user is not allowed to perform an action.
714     *
715     * @return string The error message.
716     */
717    public static function get_user_permissions_error_msg() {
718        return self::$user_permissions_error_msg;
719    }
720
721    /**
722     * The endpoint tried to partially or fully reconnect the website to WP.com.
723     *
724     * @since 1.15.0
725     *
726     * @return \WP_REST_Response|WP_Error
727     */
728    public function connection_reconnect() {
729        $response = array();
730
731        $next = null;
732
733        $result = $this->connection->restore();
734
735        if ( is_wp_error( $result ) ) {
736            $response = $result;
737        } elseif ( is_string( $result ) ) {
738            $next = $result;
739        } else {
740            $next = true === $result ? 'completed' : 'failed';
741        }
742
743        switch ( $next ) {
744            case 'authorize':
745                $response['status']       = 'in_progress';
746                $response['authorizeUrl'] = $this->connection->get_authorization_url();
747                break;
748            case 'completed':
749                $response['status'] = 'completed';
750                /**
751                 * Action fired when reconnection has completed successfully.
752                 *
753                 * @since 1.18.1
754                 */
755                do_action( 'jetpack_reconnection_completed' );
756                break;
757            case 'failed':
758                $response = new WP_Error( 'Reconnect failed' );
759                break;
760        }
761
762        return rest_ensure_response( $response );
763    }
764
765    /**
766     * Verify that user is allowed to connect Jetpack.
767     *
768     * @since 1.26.0
769     *
770     * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
771     */
772    public static function jetpack_register_permission_check() {
773        if ( current_user_can( 'jetpack_connect' ) ) {
774            return true;
775        }
776
777        return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
778    }
779
780    /**
781     * The endpoint tried to connect Jetpack site to WPCOM.
782     *
783     * @since 1.7.0
784     * @since 6.7.0 No longer needs `registration_nonce`.
785     * @since-jetpack 7.7.0
786     *
787     * @param \WP_REST_Request $request The request sent to the WP REST API.
788     *
789     * @return \WP_REST_Response|WP_Error
790     */
791    public function connection_register( $request ) {
792        if ( isset( $request['from'] ) ) {
793            $this->connection->add_register_request_param( 'from', (string) $request['from'] );
794        }
795
796        if ( ! empty( $request['plugin_slug'] ) ) {
797            // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
798            $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
799            if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
800                $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
801            }
802        }
803
804        $result = $this->connection->try_registration();
805
806        if ( is_wp_error( $result ) ) {
807            return $result;
808        }
809
810        $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
811
812        $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri );
813
814        /**
815         * Filters the response of jetpack/v4/connection/register endpoint
816         *
817         * @param array $response Array response
818         * @since 1.27.0
819         */
820        $response_body = apply_filters(
821            'jetpack_register_site_rest_response',
822            array()
823        );
824
825        // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
826        $response_body['authorizeUrl'] = $authorize_url;
827        if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
828            $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
829        }
830
831        return rest_ensure_response( $response_body );
832    }
833
834    /**
835     * Get the authorization URL.
836     *
837     * @since 1.27.0
838     *
839     * @param \WP_REST_Request $request The request sent to the WP REST API.
840     *
841     * @return \WP_REST_Response|WP_Error
842     */
843    public function connection_authorize_url( $request ) {
844        $redirect_uri  = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
845        $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri );
846
847        return rest_ensure_response(
848            array(
849                'authorizeUrl' => $authorize_url,
850            )
851        );
852    }
853
854    /**
855     * The endpoint tried to partially or fully reconnect the website to WP.com.
856     *
857     * @since 1.29.0
858     *
859     * @param \WP_REST_Request $request The request sent to the WP REST API.
860     *
861     * @return \WP_REST_Response|WP_Error
862     */
863    public static function update_user_token( $request ) {
864        $token_parts = explode( '.', $request['user_token'] );
865
866        if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
867            return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
868        }
869
870        $user_id = (int) $token_parts[2];
871
872        if ( false === get_userdata( $user_id ) ) {
873            return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
874        }
875
876        $connection = new Manager();
877
878        if ( ! $connection->is_connected() ) {
879            return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
880        }
881
882        $is_connection_owner = isset( $request['is_connection_owner'] )
883            ? (bool) $request['is_connection_owner']
884            : ( new Manager() )->get_connection_owner_id() === $user_id;
885
886        ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
887
888        /**
889         * Fires when the user token gets successfully replaced.
890         *
891         * @since 1.29.0
892         *
893         * @param int $user_id User ID.
894         * @param string $token New user token.
895         */
896        do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );
897
898        return rest_ensure_response(
899            array(
900                'success' => true,
901            )
902        );
903    }
904
905    /**
906     * Disconnects Jetpack from the WordPress.com Servers
907     *
908     * @since 1.30.1
909     *
910     * @return bool|WP_Error True if Jetpack successfully disconnected.
911     */
912    public static function disconnect_site() {
913        $connection = new Manager();
914
915        if ( $connection->is_connected() ) {
916            $connection->disconnect_site();
917            return rest_ensure_response( array( 'code' => 'success' ) );
918        }
919
920        return new WP_Error(
921            'disconnect_failed',
922            esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
923            array( 'status' => 400 )
924        );
925    }
926
927    /**
928     * Unlinks current user from the WordPress.com Servers.
929     *
930     * @since 6.3.3
931     *
932     * @param WP_REST_Request $request The request sent to the WP REST API.
933     *
934     * @return bool|WP_Error True if user successfully unlinked.
935     */
936    public static function unlink_user( $request ) {
937
938        if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
939            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
940        }
941
942        // 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.
943        $disconnect_all_users = false;
944
945        if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
946            if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
947                $disconnect_all_users = true;
948            } else {
949                return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
950            }
951        }
952
953        // Allow admins to force a disconnect by passing the "force" parameter
954        // This allows an admin to disconnect themselves
955        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 ) ) {
956            return rest_ensure_response(
957                array(
958                    'code' => 'success',
959                )
960            );
961        } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
962            return rest_ensure_response(
963                array(
964                    'code' => 'success',
965                )
966            );
967        }
968
969        return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
970    }
971
972    /**
973     * Verify that the API client is allowed to replace user token.
974     *
975     * @since 1.29.0
976     *
977     * @return bool|WP_Error
978     */
979    public static function update_user_token_permission_check() {
980        return Rest_Authentication::is_signed_with_blog_token()
981            ? true
982            : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
983    }
984
985    /**
986     * Change the connection owner.
987     *
988     * @since 1.29.0
989     *
990     * @param WP_REST_Request $request The request sent to the WP REST API.
991     *
992     * @return \WP_REST_Response|WP_Error
993     */
994    public static function set_connection_owner( $request ) {
995        $new_owner_id = $request['owner'];
996
997        $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );
998
999        if ( is_wp_error( $owner_set ) ) {
1000            return $owner_set;
1001        }
1002
1003        return rest_ensure_response(
1004            array(
1005                'code' => 'success',
1006            )
1007        );
1008    }
1009
1010    /**
1011     * Check that user has permission to change the master user.
1012     *
1013     * @since 1.7.0
1014     * @since-jetpack 6.2.0
1015     * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
1016     *
1017     * @return bool|WP_Error True if user is able to change master user.
1018     */
1019    public static function set_connection_owner_permission_check() {
1020        if ( current_user_can( 'jetpack_disconnect' ) ) {
1021            return true;
1022        }
1023
1024        return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1025    }
1026
1027    /**
1028     * The endpoint verifies blog connection and blog token validity.
1029     *
1030     * @since 2.7.0
1031     *
1032     * @return mixed|null
1033     */
1034    public function connection_check() {
1035        /**
1036         * Filters the successful response of the REST API test_connection method
1037         *
1038         * @param string $response The response string.
1039         */
1040        $status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
1041
1042        return rest_ensure_response(
1043            array(
1044                'status' => $status,
1045            )
1046        );
1047    }
1048
1049    /**
1050     * Remote connect endpoint permission check.
1051     *
1052     * @return true|WP_Error
1053     */
1054    public function connection_check_permission_check() {
1055        if ( current_user_can( 'jetpack_connect' ) ) {
1056            return true;
1057        }
1058
1059        return Rest_Authentication::is_signed_with_blog_token()
1060            ? true
1061            : new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1062    }
1063
1064    /**
1065     * Permission check for the connection/data endpoint
1066     *
1067     * @return bool|WP_Error
1068     */
1069    public static function user_connection_data_permission_check() {
1070        if ( current_user_can( 'jetpack_connect_user' ) ) {
1071            return true;
1072        }
1073
1074        return new WP_Error(
1075            'invalid_user_permission_user_connection_data',
1076            self::get_user_permissions_error_msg(),
1077            array( 'status' => rest_authorization_required_code() )
1078        );
1079    }
1080
1081    /**
1082     * Verifies if the request was signed with the Jetpack Debugger key
1083     *
1084     * @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.
1085     *
1086     * @return bool
1087     */
1088    public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
1089         // phpcs:disable WordPress.Security.NonceVerification.Recommended
1090        if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
1091            return false;
1092        }
1093
1094        // signature timestamp must be within 5min of current time.
1095        if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1096            return false;
1097        }
1098
1099        $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1100
1101        $signature_data = wp_json_encode(
1102            array(
1103                'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
1104                'timestamp'  => (int) $_GET['timestamp'],
1105                'url'        => filter_var( wp_unslash( $_GET['url'] ) ),
1106            ),
1107            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.
1108        );
1109
1110        if (
1111            ! function_exists( 'openssl_verify' )
1112            || 1 !== openssl_verify(
1113                $signature_data,
1114                $signature,
1115                $pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
1116            )
1117        ) {
1118            return false;
1119        }
1120
1121        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1122
1123        return true;
1124    }
1125}