Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.32% covered (success)
95.32%
489 / 513
56.25% covered (warning)
56.25%
18 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Connector
95.32% covered (success)
95.32%
489 / 513
56.25% covered (warning)
56.25%
18 / 32
116
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
213 / 213
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
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
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                    'from'         => array(
263                        'description' => __( 'Tracking/segmentation identifier for this authorize URL request', 'jetpack-connection' ),
264                        'type'        => 'string',
265                    ),
266                ),
267            )
268        );
269
270        register_rest_route(
271            'jetpack/v4',
272            '/user-token',
273            array(
274                array(
275                    'methods'             => WP_REST_Server::EDITABLE,
276                    'callback'            => array( static::class, 'update_user_token' ),
277                    'permission_callback' => array( static::class, 'update_user_token_permission_check' ),
278                    'args'                => array(
279                        'user_token'          => array(
280                            'description' => __( 'New user token', 'jetpack-connection' ),
281                            'type'        => 'string',
282                            'required'    => true,
283                        ),
284                        'is_connection_owner' => array(
285                            'description' => __( 'Is connection owner', 'jetpack-connection' ),
286                            'type'        => 'boolean',
287                        ),
288                    ),
289                ),
290            )
291        );
292
293        // Set the connection owner.
294        register_rest_route(
295            'jetpack/v4',
296            '/connection/owner',
297            array(
298                'methods'             => WP_REST_Server::EDITABLE,
299                'callback'            => array( static::class, 'set_connection_owner' ),
300                'permission_callback' => array( static::class, 'set_connection_owner_permission_check' ),
301                'args'                => array(
302                    'owner' => array(
303                        'description' => __( 'New owner', 'jetpack-connection' ),
304                        'type'        => 'integer',
305                        'required'    => true,
306                    ),
307                ),
308            )
309        );
310    }
311
312    /**
313     * Handles verification that a site is registered.
314     *
315     * @since 1.7.0
316     * @since-jetpack 5.4.0
317     *
318     * @param WP_REST_Request $request The request sent to the WP REST API.
319     *
320     * @return string|WP_Error
321     */
322    public function verify_registration( WP_REST_Request $request ) {
323        $registration_data = array( $request['secret_1'], $request['state'] );
324
325        return $this->connection->handle_registration( $registration_data );
326    }
327
328    /**
329     * Handles verification that a site is registered
330     *
331     * @since 1.7.0
332     * @since-jetpack 5.4.0
333     *
334     * @param WP_REST_Request $request The request sent to the WP REST API.
335     *
336     * @return array|WP_Error
337     */
338    public static function remote_authorize( $request ) {
339        $xmlrpc_server = new Jetpack_XMLRPC_Server();
340        $result        = $xmlrpc_server->remote_authorize( $request );
341
342        if ( is_a( $result, 'IXR_Error' ) ) {
343            $result = new WP_Error( $result->code, $result->message );
344        }
345
346        return $result;
347    }
348
349    /**
350     * Initiate the site provisioning process.
351     *
352     * @since 2.5.0
353     *
354     * @param WP_REST_Request $request The request sent to the WP REST API.
355     *
356     * @return WP_Error|array
357     */
358    public function remote_provision( WP_REST_Request $request ) {
359        $request_data = $request->get_params();
360
361        if ( current_user_can( 'jetpack_connect_user' ) ) {
362            $request_data['local_user'] = get_current_user_id();
363        }
364
365        $xmlrpc_server = new Jetpack_XMLRPC_Server();
366        $result        = $xmlrpc_server->remote_provision( $request_data );
367
368        if ( is_a( $result, 'IXR_Error' ) ) {
369            $result = new WP_Error( $result->code, $result->message );
370        }
371
372        return $result;
373    }
374
375    /**
376     * Connect a remote user.
377     *
378     * @since 2.6.0
379     *
380     * @param WP_REST_Request $request The request sent to the WP REST API.
381     *
382     * @return WP_Error|array
383     */
384    public static function remote_connect( WP_REST_Request $request ) {
385        $xmlrpc_server = new Jetpack_XMLRPC_Server();
386        $result        = $xmlrpc_server->remote_connect( $request );
387
388        if ( is_a( $result, 'IXR_Error' ) ) {
389            $result = new WP_Error( $result->code, $result->message );
390        }
391
392        return $result;
393    }
394
395    /**
396     * Register the site so that a plan can be provisioned.
397     *
398     * @since 2.5.0
399     *
400     * @param WP_REST_Request $request The request object.
401     *
402     * @return WP_Error|array
403     */
404    public function remote_register( WP_REST_Request $request ) {
405        $xmlrpc_server = new Jetpack_XMLRPC_Server();
406        $result        = $xmlrpc_server->remote_register( $request );
407
408        if ( is_a( $result, 'IXR_Error' ) ) {
409            $result = new WP_Error( $result->code, $result->message );
410        }
411
412        return $result;
413    }
414
415    /**
416     * Remote provision endpoint permission check.
417     *
418     * @param WP_REST_Request $request The request object.
419     *
420     * @return true|WP_Error
421     */
422    public function remote_provision_permission_check( WP_REST_Request $request ) {
423        if ( empty( $request['local_user'] ) && current_user_can( 'jetpack_connect_user' ) ) {
424            return true;
425        }
426
427        return Rest_Authentication::is_signed_with_blog_token()
428            ? true
429            : new WP_Error( 'invalid_permission_remote_provision', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
430    }
431
432    /**
433     * Remote connect endpoint permission check.
434     *
435     * @return true|WP_Error
436     */
437    public function remote_connect_permission_check() {
438        return Rest_Authentication::is_signed_with_blog_token()
439            ? true
440            : new WP_Error( 'invalid_permission_remote_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
441    }
442
443    /**
444     * Remote register endpoint permission check.
445     *
446     * @return true|WP_Error
447     */
448    public function remote_register_permission_check() {
449        if ( $this->connection->has_connected_owner() ) {
450            return Rest_Authentication::is_signed_with_blog_token()
451                ? true
452                : new WP_Error( 'already_registered', __( 'Blog is already registered', 'jetpack-connection' ), 400 );
453        }
454
455        return true;
456    }
457
458    /**
459     * Get connection status for this Jetpack site.
460     *
461     * @since 1.7.0
462     * @since-jetpack 4.3.0
463     *
464     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
465     *
466     * @return WP_REST_Response|array Connection information.
467     */
468    public static function connection_status( $rest_response = true ) {
469        $status     = new Status();
470        $connection = new Manager();
471
472        $connection_status = array(
473            'isActive'          => $connection->has_connected_owner(), // TODO deprecate this.
474            'isStaging'         => $status->in_safe_mode(), // TODO deprecate this.
475            'isRegistered'      => $connection->is_connected(),
476            'isUserConnected'   => $connection->is_user_connected(),
477            'hasConnectedOwner' => $connection->has_connected_owner(),
478            'offlineMode'       => array(
479                'isActive'        => $status->is_offline_mode(),
480                'constant'        => defined( 'JETPACK_DEV_DEBUG' ) && JETPACK_DEV_DEBUG,
481                'url'             => $status->is_local_site(),
482                /** This filter is documented in packages/status/src/class-status.php */
483                'filter'          => apply_filters( 'jetpack_offline_mode', false ),
484                'wpLocalConstant' => defined( 'WP_LOCAL_DEV' ) && WP_LOCAL_DEV,
485                'option'          => (bool) get_option( 'jetpack_offline_mode' ),
486            ),
487            'isPublic'          => '1' == get_option( 'blog_public' ), // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
488        );
489
490        /**
491         * Filters the connection status data.
492         *
493         * @since 1.25.0
494         *
495         * @param array An array containing the connection status data.
496         */
497        $connection_status = apply_filters( 'jetpack_connection_status', $connection_status );
498
499        if ( $rest_response ) {
500            return rest_ensure_response(
501                $connection_status
502            );
503        } else {
504            return $connection_status;
505        }
506    }
507
508    /**
509     * Get plugins connected to the Jetpack.
510     *
511     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
512     *
513     * @since 1.13.1
514     * @since 1.38.0 Added $rest_response param.
515     *
516     * @return WP_REST_Response|WP_Error Response or error object, depending on the request result.
517     */
518    public static function get_connection_plugins( $rest_response = true ) {
519        $plugins = ( new Manager() )->get_connected_plugins();
520
521        if ( is_wp_error( $plugins ) ) {
522            return $plugins;
523        }
524
525        array_walk(
526            $plugins,
527            function ( &$data, $slug ) {
528                $data['slug'] = $slug;
529            }
530        );
531
532        if ( $rest_response ) {
533            return rest_ensure_response( array_values( $plugins ) );
534        }
535
536        return array_values( $plugins );
537    }
538
539    /**
540     * Verify that user can view Jetpack admin page and can activate plugins.
541     *
542     * @since 1.15.0
543     *
544     * @return bool|WP_Error Whether user has the capability 'activate_plugins'.
545     */
546    public static function activate_plugins_permission_check() {
547        if ( current_user_can( 'activate_plugins' ) ) {
548            return true;
549        }
550
551        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
552    }
553
554    /**
555     * Permission check for the connection_plugins endpoint
556     *
557     * @return bool|WP_Error
558     */
559    public static function connection_plugins_permission_check() {
560        if ( true === static::activate_plugins_permission_check() ) {
561            return true;
562        }
563
564        if ( true === static::is_request_signed_by_jetpack_debugger() ) {
565            return true;
566        }
567
568        return new WP_Error( 'invalid_user_permission_activate_plugins', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
569    }
570
571    /**
572     * Permission check for the disconnect site endpoint.
573     *
574     * @since 1.30.1
575     *
576     * @since 5.1.0 Modified the permission check to accept requests signed with blog tokens.
577     *
578     * @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).
579     */
580    public static function disconnect_site_permission_check() {
581        if ( current_user_can( 'jetpack_disconnect' ) ) {
582            return true;
583        }
584
585        return Rest_Authentication::is_signed_with_blog_token()
586            ? true
587            : new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
588    }
589
590    /**
591     * Verify that a user can use the /connection/user endpoint. Has to be a registered user and be currently linked.
592     *
593     * @since 6.3.3
594     *
595     * @return bool|WP_Error True if user is able to unlink.
596     */
597    public static function unlink_user_permission_callback() {
598        // This is a mapped capability
599        // phpcs:ignore WordPress.WP.Capabilities.Unknown
600        if ( current_user_can( 'jetpack_unlink_user' ) && ( new Manager() )->is_user_connected( get_current_user_id() ) ) {
601            return true;
602        }
603
604        return new WP_Error(
605            'invalid_user_permission_unlink_user',
606            self::get_user_permissions_error_msg(),
607            array( 'status' => rest_authorization_required_code() )
608        );
609    }
610
611    /**
612     * Get miscellaneous user data related to the connection. Similar data available in old "My Jetpack".
613     * Information about the master/primary user.
614     * Information about the current user.
615     *
616     * @param bool $rest_response Should we return a rest response or a simple array. Default to rest response.
617     *
618     * @since 1.30.1
619     *
620     * @return \WP_REST_Response|array
621     */
622    public static function get_user_connection_data( $rest_response = true ) {
623        $blog_id = \Jetpack_Options::get_option( 'id' );
624
625        $connection = new Manager();
626
627        $current_user     = wp_get_current_user();
628        $connection_owner = $connection->get_connection_owner();
629
630        $owner_display_name = false === $connection_owner ? null : $connection_owner->display_name;
631
632        $is_user_connected = $connection->is_user_connected();
633        $is_master_user    = false === $connection_owner ? false : ( $current_user->ID === $connection_owner->ID );
634        $wpcom_user_data   = $connection->get_connected_user_data();
635
636        // Add connected user gravatar to the returned wpcom_user_data.
637        // Probably we shouldn't do this when $wpcom_user_data is false, but we have been since 2016 so
638        // clients probably expect that by now.
639        if ( false === $wpcom_user_data ) {
640            $wpcom_user_data = array();
641        }
642        $wpcom_user_data['avatar'] = ( ! empty( $wpcom_user_data['email'] ) ?
643        get_avatar_url(
644            $wpcom_user_data['email'],
645            array(
646                'size'    => 64,
647                'default' => 'mysteryman',
648            )
649        )
650        : false );
651
652        // Check for possible account errors between the local user and WPCOM account.
653        $possible_errors = array();
654        if ( $is_user_connected && ! empty( $wpcom_user_data['email'] ) ) {
655            $user_account_status = new \Automattic\Jetpack\Connection\User_Account_Status();
656            $possible_errors     = $user_account_status->check_account_errors( $current_user->user_email, $wpcom_user_data['email'] );
657        }
658
659        $current_user_connection_data = array(
660            'isConnected'           => $is_user_connected,
661            'isMaster'              => $is_master_user,
662            'username'              => $current_user->user_login,
663            'id'                    => $current_user->ID,
664            'blogId'                => $blog_id,
665            'wpcomUser'             => $wpcom_user_data,
666            'gravatar'              => get_avatar_url( $current_user->ID ),
667            'permissions'           => array(
668                'connect'        => current_user_can( 'jetpack_connect' ),
669                'connect_user'   => current_user_can( 'jetpack_connect_user' ),
670                // This is a mapped capability
671                // phpcs:ignore WordPress.WP.Capabilities.Unknown
672                'unlink_user'    => current_user_can( 'jetpack_unlink_user' ),
673                'disconnect'     => current_user_can( 'jetpack_disconnect' ),
674                'manage_options' => current_user_can( 'manage_options' ),
675            ),
676            'possibleAccountErrors' => $possible_errors,
677        );
678
679        /**
680         * Filters the current user connection data.
681         *
682         * @since 1.30.1
683         *
684         * @param array An array containing the current user connection data.
685         */
686        $current_user_connection_data = apply_filters( 'jetpack_current_user_connection_data', $current_user_connection_data );
687
688        $response = array(
689            'currentUser'     => $current_user_connection_data,
690            'connectionOwner' => $owner_display_name,
691            'isRegistered'    => $connection->is_connected(),
692        );
693
694        if ( $rest_response ) {
695            return rest_ensure_response( $response );
696        }
697
698        return $response;
699    }
700
701    /**
702     * Verify that user is allowed to disconnect Jetpack.
703     *
704     * @since 1.15.0
705     *
706     * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
707     */
708    public static function jetpack_reconnect_permission_check() {
709        if ( current_user_can( 'jetpack_reconnect' ) ) {
710            return true;
711        }
712
713        return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
714    }
715
716    /**
717     * Returns generic error message when user is not allowed to perform an action.
718     *
719     * @return string The error message.
720     */
721    public static function get_user_permissions_error_msg() {
722        return self::$user_permissions_error_msg;
723    }
724
725    /**
726     * The endpoint tried to partially or fully reconnect the website to WP.com.
727     *
728     * @since 1.15.0
729     *
730     * @return \WP_REST_Response|WP_Error
731     */
732    public function connection_reconnect() {
733        $response = array();
734
735        $next = null;
736
737        $result = $this->connection->restore();
738
739        if ( is_wp_error( $result ) ) {
740            $response = $result;
741        } elseif ( is_string( $result ) ) {
742            $next = $result;
743        } else {
744            $next = true === $result ? 'completed' : 'failed';
745        }
746
747        switch ( $next ) {
748            case 'authorize':
749                $response['status']       = 'in_progress';
750                $response['authorizeUrl'] = $this->connection->get_authorization_url();
751                break;
752            case 'completed':
753                $response['status'] = 'completed';
754                /**
755                 * Action fired when reconnection has completed successfully.
756                 *
757                 * @since 1.18.1
758                 */
759                do_action( 'jetpack_reconnection_completed' );
760                break;
761            case 'failed':
762                $response = new WP_Error( 'Reconnect failed' );
763                break;
764        }
765
766        return rest_ensure_response( $response );
767    }
768
769    /**
770     * Verify that user is allowed to connect Jetpack.
771     *
772     * @since 1.26.0
773     *
774     * @return bool|WP_Error Whether user has the capability 'jetpack_connect'.
775     */
776    public static function jetpack_register_permission_check() {
777        if ( current_user_can( 'jetpack_connect' ) ) {
778            return true;
779        }
780
781        return new WP_Error( 'invalid_user_permission_jetpack_connect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
782    }
783
784    /**
785     * The endpoint tried to connect Jetpack site to WPCOM.
786     *
787     * @since 1.7.0
788     * @since 6.7.0 No longer needs `registration_nonce`.
789     * @since-jetpack 7.7.0
790     *
791     * @param \WP_REST_Request $request The request sent to the WP REST API.
792     *
793     * @return \WP_REST_Response|WP_Error
794     */
795    public function connection_register( $request ) {
796        $from = isset( $request['from'] ) ? (string) $request['from'] : '';
797        if ( '' !== $from ) {
798            $this->connection->add_register_request_param( 'from', $from );
799        }
800
801        if ( ! empty( $request['plugin_slug'] ) ) {
802            // If `plugin_slug` matches a plugin using the connection, let's inform the plugin that is establishing the connection.
803            $connected_plugin = Plugin_Storage::get_one( (string) $request['plugin_slug'] );
804            if ( ! is_wp_error( $connected_plugin ) && ! empty( $connected_plugin ) ) {
805                $this->connection->set_plugin_instance( new Plugin( (string) $request['plugin_slug'] ) );
806            }
807        }
808
809        $result = $this->connection->try_registration();
810
811        if ( is_wp_error( $result ) ) {
812            return $result;
813        }
814
815        $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
816
817        $authorize_url = ( new Authorize_Redirect( $this->connection ) )->build_authorize_url( $redirect_uri, '' !== $from ? $from : false );
818
819        /**
820         * Filters the response of jetpack/v4/connection/register endpoint
821         *
822         * @param array $response Array response
823         * @since 1.27.0
824         */
825        $response_body = apply_filters(
826            'jetpack_register_site_rest_response',
827            array()
828        );
829
830        // We manipulate the alternate URLs after the filter is applied, so they cannot be overwritten.
831        $response_body['authorizeUrl'] = $authorize_url;
832        if ( ! empty( $response_body['alternateAuthorizeUrl'] ) ) {
833            $response_body['alternateAuthorizeUrl'] = Redirect::get_url( $response_body['alternateAuthorizeUrl'] );
834        }
835
836        return rest_ensure_response( $response_body );
837    }
838
839    /**
840     * Get the authorization URL.
841     *
842     * @since 1.27.0
843     *
844     * @param \WP_REST_Request $request The request sent to the WP REST API.
845     *
846     * @return \WP_REST_Response|WP_Error
847     */
848    public function connection_authorize_url( $request ) {
849        $redirect_uri  = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
850        $from          = $request->get_param( 'from' );
851        $authorize_url = $this->connection->get_authorization_url( null, $redirect_uri, ! empty( $from ) ? (string) $from : false );
852
853        return rest_ensure_response(
854            array(
855                'authorizeUrl' => $authorize_url,
856            )
857        );
858    }
859
860    /**
861     * The endpoint tried to partially or fully reconnect the website to WP.com.
862     *
863     * @since 1.29.0
864     *
865     * @param \WP_REST_Request $request The request sent to the WP REST API.
866     *
867     * @return \WP_REST_Response|WP_Error
868     */
869    public static function update_user_token( $request ) {
870        $token_parts = explode( '.', $request['user_token'] );
871
872        if ( count( $token_parts ) !== 3 || ! (int) $token_parts[2] || ! ctype_digit( $token_parts[2] ) ) {
873            return new WP_Error( 'invalid_argument_user_token', esc_html__( 'Invalid user token is provided', 'jetpack-connection' ) );
874        }
875
876        $user_id = (int) $token_parts[2];
877
878        if ( false === get_userdata( $user_id ) ) {
879            return new WP_Error( 'invalid_argument_user_id', esc_html__( 'Invalid user id is provided', 'jetpack-connection' ) );
880        }
881
882        $connection = new Manager();
883
884        if ( ! $connection->is_connected() ) {
885            return new WP_Error( 'site_not_connected', esc_html__( 'Site is not connected', 'jetpack-connection' ) );
886        }
887
888        $is_connection_owner = isset( $request['is_connection_owner'] )
889            ? (bool) $request['is_connection_owner']
890            : ( new Manager() )->get_connection_owner_id() === $user_id;
891
892        ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
893
894        /**
895         * Fires when the user token gets successfully replaced.
896         *
897         * @since 1.29.0
898         *
899         * @param int $user_id User ID.
900         * @param string $token New user token.
901         */
902        do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );
903
904        return rest_ensure_response(
905            array(
906                'success' => true,
907            )
908        );
909    }
910
911    /**
912     * Disconnects Jetpack from the WordPress.com Servers
913     *
914     * @since 1.30.1
915     *
916     * @return bool|WP_Error True if Jetpack successfully disconnected.
917     */
918    public static function disconnect_site() {
919        $connection = new Manager();
920
921        if ( $connection->is_connected() ) {
922            $connection->disconnect_site();
923            return rest_ensure_response( array( 'code' => 'success' ) );
924        }
925
926        return new WP_Error(
927            'disconnect_failed',
928            esc_html__( 'Failed to disconnect the site as it appears already disconnected.', 'jetpack-connection' ),
929            array( 'status' => 400 )
930        );
931    }
932
933    /**
934     * Unlinks current user from the WordPress.com Servers.
935     *
936     * @since 6.3.3
937     *
938     * @param WP_REST_Request $request The request sent to the WP REST API.
939     *
940     * @return bool|WP_Error True if user successfully unlinked.
941     */
942    public static function unlink_user( $request ) {
943
944        if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
945            return new WP_Error( 'invalid_param', esc_html__( 'Invalid Parameter', 'jetpack-connection' ), array( 'status' => 404 ) );
946        }
947
948        // 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.
949        $disconnect_all_users = false;
950
951        if ( ( new Manager() )->get_connection_owner_id() === get_current_user_id() ) {
952            if ( isset( $request['disconnect-all-users'] ) && false !== $request['disconnect-all-users'] ) {
953                $disconnect_all_users = true;
954            } else {
955                return new WP_Error( 'unlink_user_failed', esc_html__( 'Unable to unlink the connection owner.', 'jetpack-connection' ), array( 'status' => 400 ) );
956            }
957        }
958
959        // Allow admins to force a disconnect by passing the "force" parameter
960        // This allows an admin to disconnect themselves
961        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 ) ) {
962            return rest_ensure_response(
963                array(
964                    'code' => 'success',
965                )
966            );
967        } elseif ( ( new Manager( 'jetpack' ) )->disconnect_user() ) {
968            return rest_ensure_response(
969                array(
970                    'code' => 'success',
971                )
972            );
973        }
974
975        return new WP_Error( 'unlink_user_failed', esc_html__( 'Was not able to unlink the user. Please try again.', 'jetpack-connection' ), array( 'status' => 400 ) );
976    }
977
978    /**
979     * Verify that the API client is allowed to replace user token.
980     *
981     * @since 1.29.0
982     *
983     * @return bool|WP_Error
984     */
985    public static function update_user_token_permission_check() {
986        return Rest_Authentication::is_signed_with_blog_token()
987            ? true
988            : new WP_Error( 'invalid_permission_update_user_token', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
989    }
990
991    /**
992     * Change the connection owner.
993     *
994     * @since 1.29.0
995     *
996     * @param WP_REST_Request $request The request sent to the WP REST API.
997     *
998     * @return \WP_REST_Response|WP_Error
999     */
1000    public static function set_connection_owner( $request ) {
1001        $new_owner_id = $request['owner'];
1002
1003        $owner_set = ( new Manager() )->update_connection_owner( $new_owner_id );
1004
1005        if ( is_wp_error( $owner_set ) ) {
1006            return $owner_set;
1007        }
1008
1009        return rest_ensure_response(
1010            array(
1011                'code' => 'success',
1012            )
1013        );
1014    }
1015
1016    /**
1017     * Check that user has permission to change the master user.
1018     *
1019     * @since 1.7.0
1020     * @since-jetpack 6.2.0
1021     * @since-jetpack 7.7.0 Update so that any user with jetpack_disconnect privs can set owner.
1022     *
1023     * @return bool|WP_Error True if user is able to change master user.
1024     */
1025    public static function set_connection_owner_permission_check() {
1026        if ( current_user_can( 'jetpack_disconnect' ) ) {
1027            return true;
1028        }
1029
1030        return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1031    }
1032
1033    /**
1034     * The endpoint verifies blog connection and blog token validity.
1035     *
1036     * @since 2.7.0
1037     *
1038     * @return mixed|null
1039     */
1040    public function connection_check() {
1041        /**
1042         * Filters the successful response of the REST API test_connection method
1043         *
1044         * @param string $response The response string.
1045         */
1046        $status = apply_filters( 'jetpack_rest_connection_check_response', 'success' );
1047
1048        return rest_ensure_response(
1049            array(
1050                'status' => $status,
1051            )
1052        );
1053    }
1054
1055    /**
1056     * Remote connect endpoint permission check.
1057     *
1058     * @return true|WP_Error
1059     */
1060    public function connection_check_permission_check() {
1061        if ( current_user_can( 'jetpack_connect' ) ) {
1062            return true;
1063        }
1064
1065        return Rest_Authentication::is_signed_with_blog_token()
1066            ? true
1067            : new WP_Error( 'invalid_permission_connection_check', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1068    }
1069
1070    /**
1071     * Permission check for the connection/data endpoint
1072     *
1073     * @return bool|WP_Error
1074     */
1075    public static function user_connection_data_permission_check() {
1076        if ( current_user_can( 'jetpack_connect_user' ) ) {
1077            return true;
1078        }
1079
1080        return new WP_Error(
1081            'invalid_user_permission_user_connection_data',
1082            self::get_user_permissions_error_msg(),
1083            array( 'status' => rest_authorization_required_code() )
1084        );
1085    }
1086
1087    /**
1088     * Verifies if the request was signed with the Jetpack Debugger key
1089     *
1090     * @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.
1091     *
1092     * @return bool
1093     */
1094    public static function is_request_signed_by_jetpack_debugger( $pub_key = null ) {
1095         // phpcs:disable WordPress.Security.NonceVerification.Recommended
1096        if ( ! isset( $_GET['signature'] ) || ! isset( $_GET['timestamp'] ) || ! isset( $_GET['url'] ) || ! isset( $_GET['rest_route'] ) ) {
1097            return false;
1098        }
1099
1100        // signature timestamp must be within 5min of current time.
1101        if ( abs( time() - (int) $_GET['timestamp'] ) > 300 ) {
1102            return false;
1103        }
1104
1105        $signature = base64_decode( filter_var( wp_unslash( $_GET['signature'] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
1106
1107        $signature_data = wp_json_encode(
1108            array(
1109                'rest_route' => filter_var( wp_unslash( $_GET['rest_route'] ) ),
1110                'timestamp'  => (int) $_GET['timestamp'],
1111                'url'        => filter_var( wp_unslash( $_GET['url'] ) ),
1112            ),
1113            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.
1114        );
1115
1116        if (
1117            ! function_exists( 'openssl_verify' )
1118            || 1 !== openssl_verify(
1119                $signature_data,
1120                $signature,
1121                $pub_key ? $pub_key : static::JETPACK__DEBUGGER_PUBLIC_KEY
1122            )
1123        ) {
1124            return false;
1125        }
1126
1127        // phpcs:enable WordPress.Security.NonceVerification.Recommended
1128
1129        return true;
1130    }
1131}