Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.13% covered (success)
92.13%
82 / 89
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Server_Sandbox
92.13% covered (success)
92.13%
82 / 89
71.43% covered (warning)
71.43%
5 / 7
33.53
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 server_sandbox_request_parameters
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
8
 get_new_signature
80.77% covered (warning)
80.77%
21 / 26
0.00% covered (danger)
0.00%
0 / 1
7.35
 log_new_signature_error
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 extract_authorization_headers
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 server_sandbox
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 admin_bar_add_sandbox_item
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * The Server_Sandbox class.
4 *
5 * This feature is only useful for Automattic developers.
6 * It configures Jetpack to talk to staging/sandbox servers
7 * on WordPress.com instead of production servers.
8 *
9 * @package automattic/jetpack-sandbox
10 */
11
12namespace Automattic\Jetpack\Connection;
13
14use Automattic\Jetpack\Constants;
15
16/**
17 * The Server_Sandbox class.
18 */
19class Server_Sandbox {
20
21    /**
22     * Sets up the action hooks for the server sandbox.
23     */
24    public function init() {
25        if ( did_action( 'jetpack_server_sandbox_init' ) ) {
26            return;
27        }
28
29        add_action( 'requests-requests.before_request', array( $this, 'server_sandbox' ), 10, 4 );
30        add_action( 'admin_bar_menu', array( $this, 'admin_bar_add_sandbox_item' ), 999 );
31
32        /**
33         * Fires when the server sandbox is initialized. This action is used to ensure that
34         * the server sandbox action hooks are set up only once.
35         *
36         * @since 1.30.7
37         */
38        do_action( 'jetpack_server_sandbox_init' );
39    }
40
41    /**
42     * Returns the new url and host values.
43     *
44     * @param string $sandbox Sandbox domain.
45     * @param string $url URL of request about to be made.
46     * @param array  $headers Headers of request about to be made.
47     * @param string $data The body of request about to be made.
48     * @param string $method The method of request about to be made.
49     *
50     * @return array [ 'url' => new URL, 'host' => new Host, 'new_signature => New signature if url was changed ]
51     */
52    public function server_sandbox_request_parameters( $sandbox, $url, $headers, $data = null, $method = 'GET' ) {
53        $host          = '';
54        $new_signature = '';
55
56        if ( ! is_string( $sandbox ) || ! is_string( $url ) ) {
57            return array(
58                'url'           => $url,
59                'host'          => $host,
60                'new_signature' => $new_signature,
61            );
62        }
63
64        $url_host = wp_parse_url( $url, PHP_URL_HOST );
65
66        switch ( $url_host ) {
67            case 'public-api.wordpress.com':
68            case 'jetpack.wordpress.com':
69            case 'jetpack.com':
70            case 'dashboard.wordpress.com':
71                $host         = $headers['Host'] ?? $url_host;
72                $original_url = $url;
73                $url          = preg_replace(
74                    '@^(https?://)' . preg_quote( $url_host, '@' ) . '(?=[/?#].*|$)@',
75                    '${1}' . $sandbox,
76                    $url,
77                    1
78                );
79
80                /**
81                 * Whether to add the X Debug query parameter to the request made to the Sandbox
82                 *
83                 * @since 1.36.0
84                 *
85                 * @param bool   $add_parameter Whether to add the parameter to the request or not. Default is to false.
86                 * @param string $url The URL of the request being made.
87                 * @param string $host The host of the request being made.
88                */
89                if ( apply_filters( 'jetpack_sandbox_add_profile_parameter', false, $url, $host ) ) {
90                    $url = add_query_arg( 'XDEBUG_PROFILE', 1, $url );
91
92                    // URL has been modified since the signature was created. We'll need a new one.
93                    $original_url  = add_query_arg( 'XDEBUG_PROFILE', 1, $original_url );
94                    $new_signature = $this->get_new_signature( $original_url, $headers, $data, $method );
95
96                }
97        }
98
99        return compact( 'url', 'host', 'new_signature' );
100    }
101
102    /**
103     * Gets a new signature for the request
104     *
105     * @param string $url The new URL to be signed.
106     * @param array  $headers The headers of the request about to be made.
107     * @param string $data The body of request about to be made.
108     * @param string $method The method of the request about to be made.
109     * @return string|null
110     */
111    private function get_new_signature( $url, $headers, $data, $method ) {
112
113        if ( ! empty( $headers['Authorization'] ) ) {
114            $a_headers = $this->extract_authorization_headers( $headers );
115            if ( ! empty( $a_headers ) ) {
116                $token_details = explode( ':', $a_headers['token'] );
117
118                if ( count( $token_details ) === 3 ) {
119                    $user_id           = $token_details[2];
120                    $token             = ( new Tokens() )->get_access_token( $user_id );
121                    $time_diff         = (int) \Jetpack_Options::get_option( 'time_diff' );
122                    $jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff );
123
124                    $signature = $jetpack_signature->sign_request(
125                        $a_headers['token'],
126                        $a_headers['timestamp'],
127                        $a_headers['nonce'],
128                        $a_headers['body-hash'],
129                        $method,
130                        $url,
131                        $data,
132                        false
133                    );
134
135                    if ( $signature && ! is_wp_error( $signature ) ) {
136                        return $signature;
137                    } elseif ( is_wp_error( $signature ) ) {
138                        $this->log_new_signature_error( $signature->get_error_message() );
139                    }
140                } else {
141                    $this->log_new_signature_error( 'Malformed token on Authorization Header' );
142                }
143            } else {
144                $this->log_new_signature_error( 'Error extracting Authorization Header' );
145            }
146        } else {
147            $this->log_new_signature_error( 'Empty Authorization Header' );
148        }
149    }
150
151    /**
152     * Logs error if the attempt to create a new signature fails
153     *
154     * @param string $message The error message.
155     * @return void
156     */
157    private function log_new_signature_error( $message ) {
158        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
159            error_log( sprintf( "SANDBOXING: Error re-signing the request. '%s'", $message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
160        }
161    }
162
163    /**
164     * Extract the values in the Authorization header into an array
165     *
166     * @param array $headers The headers of the request about to be made.
167     * @return array|null
168     */
169    public function extract_authorization_headers( $headers ) {
170        if ( ! empty( $headers['Authorization'] ) && is_string( $headers['Authorization'] ) ) {
171            $header = str_replace( 'X_JETPACK ', '', $headers['Authorization'] );
172            $vars   = explode( ' ', $header );
173            $result = array();
174            foreach ( $vars as $var ) {
175                $elements = explode( '"', $var );
176                if ( count( $elements ) === 3 ) {
177                    $result[ substr( $elements[0], 0, -1 ) ] = $elements[1];
178                }
179            }
180            return $result;
181        }
182    }
183
184    /**
185     * Modifies parameters of request in order to send the request to the
186     * server specified by `JETPACK__SANDBOX_DOMAIN`.
187     *
188     * Attached to the `requests-requests.before_request` filter.
189     *
190     * @param string       $url URL of request about to be made.
191     * @param array        $headers Headers of request about to be made.
192     * @param array|string $data Data of request about to be made.
193     * @param string       $type Type of request about to be made.
194     * @return void
195     */
196    public function server_sandbox( &$url, &$headers, &$data = null, &$type = null ) {
197        if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) {
198            return;
199        }
200
201        $original_url = $url;
202
203        $request_parameters = $this->server_sandbox_request_parameters( Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $url, $headers, $data, $type );
204
205        $url = $request_parameters['url'];
206
207        if ( $request_parameters['host'] ) {
208            $headers['Host'] = $request_parameters['host'];
209
210            if ( $request_parameters['new_signature'] ) {
211                $headers['Authorization'] = preg_replace( '/signature=\"[^\"]+\"/', 'signature="' . $request_parameters['new_signature'] . '"', $headers['Authorization'] );
212            }
213
214            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
215                error_log( sprintf( "SANDBOXING via '%s': '%s'", Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ), $original_url ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
216            }
217        }
218    }
219
220    /**
221     * Adds a "Jetpack API Sandboxed" item to the admin bar if the JETPACK__SANDBOX_DOMAIN
222     * constant is set.
223     *
224     * Attached to the `admin_bar_menu` action.
225     *
226     * @param \WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
227     */
228    public function admin_bar_add_sandbox_item( $wp_admin_bar ) {
229        if ( ! Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ) ) {
230            return;
231        }
232
233        $node = array(
234            'id'    => 'jetpack-connection-api-sandbox',
235            'title' => 'Jetpack API Sandboxed',
236            'meta'  => array(
237                'title' => 'Sandboxing via ' . Constants::get_constant( 'JETPACK__SANDBOX_DOMAIN' ),
238            ),
239        );
240
241        $wp_admin_bar->add_menu( $node );
242    }
243}