Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Authorize_Json_Api
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 4
702
0.00% covered (danger)
0.00%
0 / 1
 verify_json_api_authorization_request
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 1
552
 add_token_to_login_redirect_json_api_authorization
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 store_json_api_authorization_token
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 login_message_json_api_authorization
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Authorize_Json_Api handler class.
4 * Used to handle connections via JSON API.
5 * Ported from the Jetpack class.
6 *
7 * @since 2.7.6 Ported from the Jetpack class.
8 *
9 * @package automattic/jetpack-connection
10 */
11
12namespace Automattic\Jetpack\Connection;
13
14use Automattic\Jetpack\Redirect;
15use Automattic\Jetpack\Status\Host;
16use Jetpack_Options;
17
18/**
19 * Authorize_Json_Api handler class.
20 */
21class Authorize_Json_Api {
22    /**
23     * Verified data for JSON authorization request
24     *
25     * @since 2.7.6
26     *
27     * @var array
28     */
29    public $json_api_authorization_request = array();
30
31    /**
32     * Verifies the request by checking the signature
33     *
34     * @since jetpack-4.6.0 Method was updated to use `$_REQUEST` instead of `$_GET` and `$_POST`. Method also updated to allow
35     * passing in an `$environment` argument that overrides `$_REQUEST`. This was useful for integrating with SSO.
36     * @since 2.7.6 Ported from Jetpack to the Connection package.
37     *
38     * @param null|array $environment Value to override $_REQUEST.
39     *
40     * @return void
41     */
42    public function verify_json_api_authorization_request( $environment = null ) {
43        $environment = $environment === null
44            ? $_REQUEST // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verification handled later in function and request data are 1) used to verify a cryptographic signature of the request data and 2) sanitized later in function.
45            : $environment;
46
47        if ( ! isset( $environment['token'] ) ) {
48            wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
49        }
50
51        list( $env_token,, $env_user_id ) = explode( ':', $environment['token'] );
52        $token                            = ( new Tokens() )->get_access_token( (int) $env_user_id, $env_token );
53        if ( ! $token || empty( $token->secret ) ) {
54            wp_die( esc_html__( 'You must connect your Jetpack plugin to WordPress.com to use this feature.', 'jetpack-connection' ) );
55        }
56
57        $die_error = __( 'Someone may be trying to trick you into giving them access to your site. Or it could be you just encountered a bug :).  Either way, please close this window.', 'jetpack-connection' );
58
59        // Host has encoded the request URL, probably as a result of a bad http => https redirect.
60        if (
61            preg_match( '/https?%3A%2F%2F/i', esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) > 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no site changes, we're erroring out.
62        ) {
63            /**
64             * Jetpack authorisation request Error.
65             *
66             * @since jetpack-7.5.0
67             */
68            do_action( 'jetpack_verify_api_authorization_request_error_double_encode' );
69            $die_error = sprintf(
70                /* translators: %s is a URL */
71                __( 'Your site is incorrectly double-encoding redirects from http to https. This is preventing Jetpack from authenticating your connection. Please visit our <a href="%s">support page</a> for details about how to resolve this.', 'jetpack-connection' ),
72                esc_url( Redirect::get_url( 'jetpack-support-double-encoding' ) )
73            );
74        }
75
76        $jetpack_signature = new \Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) );
77
78        if ( isset( $environment['jetpack_json_api_original_query'] ) ) {
79            $signature = $jetpack_signature->sign_request(
80                $environment['token'],
81                $environment['timestamp'],
82                $environment['nonce'],
83                '',
84                'GET',
85                $environment['jetpack_json_api_original_query'],
86                null,
87                true
88            );
89        } else {
90            $signature = $jetpack_signature->sign_current_request(
91                array(
92                    'body'   => null,
93                    'method' => 'GET',
94                )
95            );
96        }
97
98        if ( ! $signature ) {
99            wp_die(
100                wp_kses(
101                    $die_error,
102                    array(
103                        'a' => array(
104                            'href' => array(),
105                        ),
106                    )
107                )
108            );
109        } elseif ( is_wp_error( $signature ) ) {
110            wp_die(
111                wp_kses(
112                    $die_error,
113                    array(
114                        'a' => array(
115                            'href' => array(),
116                        ),
117                    )
118                )
119            );
120        } elseif ( ! hash_equals( $signature, $environment['signature'] ) ) {
121            if ( is_ssl() ) {
122                // If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well.
123                $signature = $jetpack_signature->sign_current_request(
124                    array(
125                        'scheme' => 'http',
126                        'body'   => null,
127                        'method' => 'GET',
128                    )
129                );
130                if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) {
131                    wp_die(
132                        wp_kses(
133                            $die_error,
134                            array(
135                                'a' => array(
136                                    'href' => array(),
137                                ),
138                            )
139                        )
140                    );
141                }
142            } else {
143                wp_die(
144                    wp_kses(
145                        $die_error,
146                        array(
147                            'a' => array(
148                                'href' => array(),
149                            ),
150                        )
151                    )
152                );
153            }
154        }
155
156        $timestamp = (int) $environment['timestamp'];
157        $nonce     = stripslashes( (string) $environment['nonce'] );
158
159        if ( ! ( new Nonce_Handler() )->add( $timestamp, $nonce ) ) {
160            // De-nonce the nonce, at least for 5 minutes.
161            // We have to reuse this nonce at least once (used the first time when the initial request is made, used a second time when the login form is POSTed).
162            $old_nonce_time = get_option( "jetpack_nonce_{$timestamp}_{$nonce}" );
163            if ( $old_nonce_time < time() - 300 ) {
164                wp_die( esc_html__( 'The authorization process expired. Please go back and try again.', 'jetpack-connection' ) );
165            }
166        }
167
168        $data         = json_decode(
169            base64_decode( stripslashes( $environment['data'] ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
170        );
171        $data_filters = array(
172            'state'        => 'opaque',
173            'client_id'    => 'int',
174            'client_title' => 'string',
175            'client_image' => 'url',
176        );
177
178        foreach ( $data_filters as $key => $sanitation ) {
179            if ( ! isset( $data->$key ) ) {
180                wp_die(
181                    wp_kses(
182                        $die_error,
183                        array(
184                            'a' => array(
185                                'href' => array(),
186                            ),
187                        )
188                    )
189                );
190            }
191
192            switch ( $sanitation ) {
193                case 'int':
194                    $this->json_api_authorization_request[ $key ] = (int) $data->$key;
195                    break;
196                case 'opaque':
197                    $this->json_api_authorization_request[ $key ] = (string) $data->$key;
198                    break;
199                case 'string':
200                    $this->json_api_authorization_request[ $key ] = wp_kses( (string) $data->$key, array() );
201                    break;
202                case 'url':
203                    $this->json_api_authorization_request[ $key ] = esc_url_raw( (string) $data->$key );
204                    break;
205            }
206        }
207
208        if ( empty( $this->json_api_authorization_request['client_id'] ) ) {
209            wp_die(
210                wp_kses(
211                    $die_error,
212                    array(
213                        'a' => array(
214                            'href' => array(),
215                        ),
216                    )
217                )
218            );
219        }
220    }
221
222    /**
223     * Add the Access Code details to the public-api.wordpress.com redirect.
224     *
225     * @since 2.7.6 Ported from Jetpack to the Connection package.
226     *
227     * @param string   $redirect_to URL.
228     * @param string   $original_redirect_to URL.
229     * @param \WP_User $user WP_User for the redirect.
230     *
231     * @return string
232     */
233    public function add_token_to_login_redirect_json_api_authorization( $redirect_to, $original_redirect_to, $user ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
234        return add_query_arg(
235            urlencode_deep(
236                array(
237                    'jetpack-code'    => get_user_meta(
238                        $user->ID,
239                        'jetpack_json_api_' . $this->json_api_authorization_request['client_id'],
240                        true
241                    ),
242                    'jetpack-user-id' => (int) $user->ID,
243                    'jetpack-state'   => $this->json_api_authorization_request['state'],
244                )
245            ),
246            $redirect_to
247        );
248    }
249
250    /**
251     * If someone logs in to approve API access, store the Access Code in usermeta.
252     *
253     * @since 2.7.6 Ported from Jetpack to the Connection package.
254     *
255     * @param string   $user_login Unused.
256     * @param \WP_User $user User logged in.
257     *
258     * @return void
259     */
260    public function store_json_api_authorization_token( $user_login, $user ) {
261        add_filter( 'login_redirect', array( $this, 'add_token_to_login_redirect_json_api_authorization' ), 10, 3 );
262        add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_public_api_domain' ) );
263        $token = wp_generate_password( 32, false );
264        update_user_meta( $user->ID, 'jetpack_json_api_' . $this->json_api_authorization_request['client_id'], $token );
265    }
266
267    /**
268     * HTML for the JSON API authorization notice.
269     *
270     * @since 2.7.6 Ported from Jetpack to the Connection package.
271     *
272     * @return string
273     */
274    public function login_message_json_api_authorization() {
275        return '<p class="message">' . sprintf(
276            /* translators: Name/image of the client requesting authorization */
277            esc_html__( '%s wants to access your site’s data. Log in to authorize that access.', 'jetpack-connection' ),
278            '<strong>' . esc_html( $this->json_api_authorization_request['client_title'] ) . '</strong>'
279        ) . '<img src="' . esc_url( $this->json_api_authorization_request['client_image'] ) . '" /></p>';
280    }
281}