Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.43% covered (warning)
85.43%
129 / 151
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Endpoints
85.43% covered (warning)
85.43%
129 / 151
70.00% covered (warning)
70.00%
7 / 10
25.78
0.00% covered (danger)
0.00%
0 / 1
 initialize_rest_api
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
1 / 1
1
 confirm_safe_mode
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
2.29
 migrate_stats_and_subscribers
41.18% covered (danger)
41.18%
7 / 17
0.00% covered (danger)
0.00%
0 / 1
10.09
 start_fresh_connection
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 identity_crisis_mitigation_permission_check
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 validate_urls_and_set_secret
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 fetch_url_secret
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 compare_url_secret
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 url_secret_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 compare_url_secret_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Identity_Crisis REST endpoints of the Connection package.
4 *
5 * @package  automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\IdentityCrisis;
9
10use Automattic\Jetpack\Connection\Manager as Connection_Manager;
11use Automattic\Jetpack\Connection\Rest_Authentication;
12use Jetpack_Options;
13use Jetpack_XMLRPC_Server;
14use WP_Error;
15use WP_REST_Server;
16
17/**
18 * This class will handle Identity Crisis Endpoints
19 *
20 * @since automattic/jetpack-identity-crisis:0.2.0
21 * @since 2.9.0
22 */
23class REST_Endpoints {
24
25    /**
26     * Initialize REST routes.
27     */
28    public static function initialize_rest_api() {
29
30        // Confirm that a site in identity crisis should be in staging mode.
31        register_rest_route(
32            'jetpack/v4',
33            '/identity-crisis/confirm-safe-mode',
34            array(
35                'methods'             => WP_REST_Server::EDITABLE,
36                'callback'            => __CLASS__ . '::confirm_safe_mode',
37                'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
38            )
39        );
40
41        // Handles the request to migrate stats and subscribers during an identity crisis.
42        register_rest_route(
43            'jetpack/v4',
44            'identity-crisis/migrate',
45            array(
46                'methods'             => WP_REST_Server::EDITABLE,
47                'callback'            => __CLASS__ . '::migrate_stats_and_subscribers',
48                'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
49            )
50        );
51
52        // IDC resolve: create an entirely new shadow site for this URL.
53        register_rest_route(
54            'jetpack/v4',
55            '/identity-crisis/start-fresh',
56            array(
57                'methods'             => WP_REST_Server::EDITABLE,
58                'callback'            => __CLASS__ . '::start_fresh_connection',
59                'permission_callback' => __CLASS__ . '::identity_crisis_mitigation_permission_check',
60                'args'                => array(
61                    'redirect_uri' => array(
62                        'description' => __( 'URI of the admin page where the user should be redirected after connection flow', 'jetpack-connection' ),
63                        'type'        => 'string',
64                    ),
65                ),
66            )
67        );
68
69        // Fetch URL and secret for IDC check.
70        register_rest_route(
71            'jetpack/v4',
72            '/identity-crisis/idc-url-validation',
73            array(
74                'methods'             => WP_REST_Server::READABLE,
75                'callback'            => array( static::class, 'validate_urls_and_set_secret' ),
76                'permission_callback' => array( static::class, 'url_secret_permission_check' ),
77            )
78        );
79
80        // Fetch URL verification secret.
81        register_rest_route(
82            'jetpack/v4',
83            '/identity-crisis/url-secret',
84            array(
85                'methods'             => WP_REST_Server::READABLE,
86                'callback'            => array( static::class, 'fetch_url_secret' ),
87                'permission_callback' => array( static::class, 'url_secret_permission_check' ),
88            )
89        );
90
91        // Fetch URL verification secret.
92        register_rest_route(
93            'jetpack/v4',
94            '/identity-crisis/compare-url-secret',
95            array(
96                'methods'             => WP_REST_Server::EDITABLE,
97                'callback'            => array( static::class, 'compare_url_secret' ),
98                'permission_callback' => array( static::class, 'compare_url_secret_permission_check' ),
99                'args'                => array(
100                    'secret' => array(
101                        'description' => __( 'URL secret to compare to the ones stored in the database.', 'jetpack-connection' ),
102                        'type'        => 'string',
103                        'required'    => true,
104                    ),
105                ),
106            )
107        );
108    }
109
110    /**
111     * Handles identity crisis mitigation, confirming safe mode for this site.
112     *
113     * @since 0.2.0
114     * @since-jetpack 4.4.0
115     *
116     * @return bool | WP_Error True if option is properly set.
117     */
118    public static function confirm_safe_mode() {
119        $updated = Jetpack_Options::update_option( 'safe_mode_confirmed', true );
120        if ( $updated ) {
121            return rest_ensure_response(
122                array(
123                    'code' => 'success',
124                )
125            );
126        }
127
128        return new WP_Error(
129            'error_setting_jetpack_safe_mode',
130            esc_html__( 'Could not confirm safe mode.', 'jetpack-connection' ),
131            array( 'status' => 500 )
132        );
133    }
134
135    /**
136     * Handles identity crisis mitigation, migrating stats and subscribers from old url to this, new url.
137     *
138     * @since 0.2.0
139     * @since-jetpack 4.4.0
140     *
141     * @return bool | WP_Error True if option is properly set.
142     */
143    public static function migrate_stats_and_subscribers() {
144        if ( Jetpack_Options::get_option( 'sync_error_idc' ) && ! Jetpack_Options::delete_option( 'sync_error_idc' ) ) {
145            return new WP_Error(
146                'error_deleting_sync_error_idc',
147                esc_html__( 'Could not delete sync error option.', 'jetpack-connection' ),
148                array( 'status' => 500 )
149            );
150        }
151
152        if ( Jetpack_Options::get_option( 'migrate_for_idc' ) || Jetpack_Options::update_option( 'migrate_for_idc', true ) ) {
153            return rest_ensure_response(
154                array(
155                    'code' => 'success',
156                )
157            );
158        }
159        return new WP_Error(
160            'error_setting_jetpack_migrate',
161            esc_html__( 'Could not confirm migration.', 'jetpack-connection' ),
162            array( 'status' => 500 )
163        );
164    }
165
166    /**
167     * This IDC resolution will disconnect the site and re-connect to a completely new
168     * and separate shadow site than the original.
169     *
170     * It will first will disconnect the site without phoning home as to not disturb the production site.
171     * It then builds a fresh connection URL and sends it back along with the response.
172     *
173     * @since 0.2.0
174     * @since-jetpack 4.4.0
175     *
176     * @param \WP_REST_Request $request The request sent to the WP REST API.
177     *
178     * @return \WP_REST_Response|WP_Error
179     */
180    public static function start_fresh_connection( $request ) {
181        /**
182         * Fires when Users have requested through Identity Crisis for the connection to be reset.
183         * Should be used to disconnect any connections and reset options.
184         *
185         * @since 0.2.0
186         */
187        do_action( 'jetpack_idc_disconnect' );
188
189        $connection = new Connection_Manager();
190        $result     = $connection->try_registration( true );
191
192        // early return if site registration fails.
193        if ( ! $result || is_wp_error( $result ) ) {
194            return rest_ensure_response( $result );
195        }
196
197        $redirect_uri = $request->get_param( 'redirect_uri' ) ? admin_url( $request->get_param( 'redirect_uri' ) ) : null;
198
199        /**
200         * Filters the connection url that users should be redirected to for re-establishing their connection.
201         *
202         * @since 0.2.0
203         *
204         * @param \WP_REST_Response|WP_Error    $connection_url Connection URL user should be redirected to.
205         */
206        return apply_filters( 'jetpack_idc_authorization_url', rest_ensure_response( $connection->get_authorization_url( null, $redirect_uri ) ) );
207    }
208
209    /**
210     * Verify that user can mitigate an identity crisis.
211     *
212     * @since 0.2.0
213     * @since-jetpack 4.4.0
214     *
215     * @return true|WP_Error True if the user has capability 'jetpack_disconnect', an error object otherwise.
216     */
217    public static function identity_crisis_mitigation_permission_check() {
218        if ( current_user_can( 'jetpack_disconnect' ) ) {
219            return true;
220        }
221        $error_msg = esc_html__(
222            'You do not have the correct user permissions to perform this action.
223            Please contact your site admin if you think this is a mistake.',
224            'jetpack-connection'
225        );
226
227        return new WP_Error( 'invalid_user_permission_identity_crisis', $error_msg, array( 'status' => rest_authorization_required_code() ) );
228    }
229
230    /**
231     * Endpoint for URL validation and creating a secret.
232     *
233     * @since 0.18.0
234     *
235     * @return array
236     */
237    public static function validate_urls_and_set_secret() {
238        $xmlrpc_server = new Jetpack_XMLRPC_Server();
239        $result        = $xmlrpc_server->validate_urls_for_idc_mitigation();
240
241        return $result;
242    }
243
244    /**
245     * Endpoint for fetching the existing secret.
246     *
247     * @return WP_Error|\WP_REST_Response
248     */
249    public static function fetch_url_secret() {
250        $secret = new URL_Secret();
251
252        if ( ! $secret->exists() ) {
253            return new WP_Error( 'missing_url_secret', esc_html__( 'URL secret does not exist.', 'jetpack-connection' ) );
254        }
255
256        return rest_ensure_response(
257            array(
258                'code' => 'success',
259                'data' => array(
260                    'secret'     => $secret->get_secret(),
261                    'expires_at' => $secret->get_expires_at(),
262                ),
263            )
264        );
265    }
266
267    /**
268     * Endpoint for comparing the existing secret.
269     *
270     * @param \WP_REST_Request $request The request sent to the WP REST API.
271     *
272     * @return WP_Error|\WP_REST_Response
273     */
274    public static function compare_url_secret( $request ) {
275        $match = false;
276
277        $storage = new URL_Secret();
278
279        if ( $storage->exists() ) {
280            $remote_secret = $request->get_param( 'secret' );
281            $match         = $remote_secret && hash_equals( $storage->get_secret(), $remote_secret );
282        }
283
284        return rest_ensure_response(
285            array(
286                'code'  => 'success',
287                'match' => $match,
288            )
289        );
290    }
291
292    /**
293     * Verify url_secret create/fetch permissions (valid blog token authentication).
294     *
295     * @return true|WP_Error
296     */
297    public static function url_secret_permission_check() {
298        return Rest_Authentication::is_signed_with_blog_token()
299            ? true
300            : new WP_Error(
301                'invalid_user_permission_identity_crisis',
302                esc_html__( 'You do not have the correct user permissions to perform this action.', 'jetpack-connection' ),
303                array( 'status' => rest_authorization_required_code() )
304            );
305    }
306
307    /**
308     * The endpoint is only available on non-connected sites.
309     * use `/identity-crisis/url-secret` for connected sites.
310     *
311     * @return true|WP_Error
312     */
313    public static function compare_url_secret_permission_check() {
314        return ( new Connection_Manager() )->is_connected()
315            ? new WP_Error(
316                'invalid_connection_status',
317                esc_html__( 'The endpoint is not available on connected sites.', 'jetpack-connection' ),
318                array( 'status' => 403 )
319            )
320            : true;
321    }
322}