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