PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 6.2.2
Jetpack – WP Security, Backup, Speed, & Growth v6.2.2
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.jetpack-client.php

class.jetpack-client.php in Jetpack – WP Security, Backup, Speed, & Growth 6.2.2, at class.jetpack-client.php

400 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class Jetpack_Client {
4 const WPCOM_JSON_API_VERSION = '1.1';
5
6 /**
7 * Makes an authorized remote request using Jetpack_Signature
8 *
9 * @return array|WP_Error WP HTTP response on success
10 */
11 public static function remote_request( $args, $body = null ) {
12 $defaults = array(
13 'url' => '',
14 'user_id' => 0,
15 'blog_id' => 0,
16 'auth_location' => JETPACK_CLIENT__AUTH_LOCATION,
17 'method' => 'POST',
18 'timeout' => 10,
19 'redirection' => 0,
20 'headers' => array(),
21 'stream' => false,
22 'filename' => null,
23 'sslverify' => true,
24 );
25
26 $args = wp_parse_args( $args, $defaults );
27
28 $args['blog_id'] = (int) $args['blog_id'];
29
30 if ( 'header' != $args['auth_location'] ) {
31 $args['auth_location'] = 'query_string';
32 }
33
34 $token = Jetpack_Data::get_access_token( $args['user_id'] );
35 if ( !$token ) {
36 return new Jetpack_Error( 'missing_token' );
37 }
38
39 $method = strtoupper( $args['method'] );
40
41 $timeout = intval( $args['timeout'] );
42
43 $redirection = $args['redirection'];
44 $stream = $args['stream'];
45 $filename = $args['filename'];
46 $sslverify = $args['sslverify'];
47
48 $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
49
50 @list( $token_key, $secret ) = explode( '.', $token->secret );
51 if ( empty( $token ) || empty( $secret ) ) {
52 return new Jetpack_Error( 'malformed_token' );
53 }
54
55 $token_key = sprintf( '%s:%d:%d', $token_key, JETPACK__API_VERSION, $token->external_user_id );
56
57 require_once JETPACK__PLUGIN_DIR . 'class.jetpack-signature.php';
58
59 $time_diff = (int) Jetpack_Options::get_option( 'time_diff' );
60 $jetpack_signature = new Jetpack_Signature( $token->secret, $time_diff );
61
62 $timestamp = time() + $time_diff;
63
64 if( function_exists( 'wp_generate_password' ) ) {
65 $nonce = wp_generate_password( 10, false );
66 } else {
67 $nonce = substr( sha1( rand( 0, 1000000 ) ), 0, 10);
68 }
69
70 // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing
71 if ( is_null( $body ) ) {
72 $body_hash = '';
73
74 } else {
75 // Allow arrays to be used in passing data.
76 $body_to_hash = $body;
77
78 if ( is_array( $body ) ) {
79 // We cast this to a new variable, because the array form of $body needs to be
80 // maintained so it can be passed into the request later on in the code.
81 if ( count( $body ) > 0 ) {
82 $body_to_hash = json_encode( self::_stringify_data( $body ) );
83 } else {
84 $body_to_hash = '';
85 }
86 }
87
88 if ( ! is_string( $body_to_hash ) ) {
89 return new Jetpack_Error( 'invalid_body', 'Body is malformed.' );
90 }
91
92 $body_hash = jetpack_sha1_base64( $body_to_hash );
93 }
94
95 $auth = array(
96 'token' => $token_key,
97 'timestamp' => $timestamp,
98 'nonce' => $nonce,
99 'body-hash' => $body_hash,
100 );
101
102 if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) {
103 $url_args = array(
104 'for' => 'jetpack',
105 'wpcom_blog_id' => Jetpack_Options::get_option( 'id' ),
106 );
107 } else {
108 $url_args = array();
109 }
110
111 if ( 'header' != $args['auth_location'] ) {
112 $url_args += $auth;
113 }
114
115 $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
116 $url = Jetpack::fix_url_for_bad_hosts( $url );
117
118 $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
119
120 if ( !$signature || is_wp_error( $signature ) ) {
121 return $signature;
122 }
123
124 // Send an Authorization header so various caches/proxies do the right thing
125 $auth['signature'] = $signature;
126 $auth['version'] = JETPACK__VERSION;
127 $header_pieces = array();
128 foreach ( $auth as $key => $value ) {
129 $header_pieces[] = sprintf( '%s="%s"', $key, $value );
130 }
131 $request['headers'] = array_merge( $args['headers'], array(
132 'Authorization' => "X_JETPACK " . join( ' ', $header_pieces ),
133 ) );
134
135 $host = parse_url( $url, PHP_URL_HOST );
136
137 // If we have a JETPACK__WPCOM_JSON_API_HOST_HEADER set, then let's use
138 // that, otherwise, let's fallback to the standard.
139 if ( defined( 'JETPACK__WPCOM_JSON_API_HOST_HEADER' ) && JETPACK__WPCOM_JSON_API_HOST_HEADER ) {
140 $request['headers']['Host'] = JETPACK__WPCOM_JSON_API_HOST_HEADER;
141
142 } elseif ( $host === JETPACK__WPCOM_JSON_API_HOST ) {
143 $request['headers']['Host'] = 'public-api.wordpress.com';
144 }
145
146 if ( 'header' != $args['auth_location'] ) {
147 $url = add_query_arg( 'signature', urlencode( $signature ), $url );
148 }
149
150 return Jetpack_Client::_wp_remote_request( $url, $request );
151 }
152
153 /**
154 * Wrapper for wp_remote_request(). Turns off SSL verification for certain SSL errors.
155 * This is lame, but many, many, many hosts have misconfigured SSL.
156 *
157 * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if:
158 * 1. a certificate error is found AND
159 * 2. not verifying the certificate works around the problem.
160 *
161 * The option is checked on each request.
162 *
163 * @internal
164 * @see Jetpack::fix_url_for_bad_hosts()
165 *
166 * @return array|WP_Error WP HTTP response on success
167 */
168 public static function _wp_remote_request( $url, $args, $set_fallback = false ) {
169 /**
170 * SSL verification (`sslverify`) for the JetpackClient remote request
171 * defaults to off, use this filter to force it on.
172 *
173 * Return `true` to ENABLE SSL verification, return `false`
174 * to DISABLE SSL verification.
175 *
176 * @since 3.6.0
177 *
178 * @param bool Whether to force `sslverify` or not.
179 */
180 if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) {
181 return wp_remote_request( $url, $args );
182 }
183
184 $fallback = Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
185 if ( false === $fallback ) {
186 Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 );
187 }
188
189 if ( (int) $fallback ) {
190 // We're flagged to fallback
191 $args['sslverify'] = false;
192 }
193
194 $response = wp_remote_request( $url, $args );
195
196 if (
197 !$set_fallback // We're not allowed to set the flag on this request, so whatever happens happens
198 ||
199 isset( $args['sslverify'] ) && !$args['sslverify'] // No verification - no point in doing it again
200 ||
201 !is_wp_error( $response ) // Let it ride
202 ) {
203 Jetpack_Client::set_time_diff( $response, $set_fallback );
204 return $response;
205 }
206
207 // At this point, we're not flagged to fallback and we are allowed to set the flag on this request.
208
209 $message = $response->get_error_message();
210
211 // Is it an SSL Certificate verification error?
212 if (
213 false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error
214 &&
215 false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error
216 &&
217 false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found
218 &&
219 false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful
220 // different versions of curl have different error messages
221 // this string should catch them all
222 &&
223 false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights
224 ) {
225 // No, it is not.
226 return $response;
227 }
228
229 // Redo the request without SSL certificate verification.
230 $args['sslverify'] = false;
231 $response = wp_remote_request( $url, $args );
232
233 if ( !is_wp_error( $response ) ) {
234 // The request went through this time, flag for future fallbacks
235 Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() );
236 Jetpack_Client::set_time_diff( $response, $set_fallback );
237 }
238
239 return $response;
240 }
241
242 public static function set_time_diff( &$response, $force_set = false ) {
243 $code = wp_remote_retrieve_response_code( $response );
244
245 // Only trust the Date header on some responses
246 if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) {
247 return;
248 }
249
250 if ( !$date = wp_remote_retrieve_header( $response, 'date' ) ) {
251 return;
252 }
253
254 if ( 0 >= $time = (int) strtotime( $date ) ) {
255 return;
256 }
257
258 $time_diff = $time - time();
259
260 if ( $force_set ) { // during register
261 Jetpack_Options::update_option( 'time_diff', $time_diff );
262 } else { // otherwise
263 $old_diff = Jetpack_Options::get_option( 'time_diff' );
264 if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) {
265 Jetpack_Options::update_option( 'time_diff', $time_diff );
266 }
267 }
268 }
269
270 /**
271 * Queries the WordPress.com REST API with a user token.
272 *
273 * @param string $path REST API path.
274 * @param string $version REST API version. Default is `2`.
275 * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
276 * @param string $body Body passed to {@see WP_Http}. Default is `null`.
277 * @param string $base_api_path REST API root. Default is `wpcom`.
278 *
279 * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
280 */
281 public static function wpcom_json_api_request_as_user( $path, $version = '2', $args = array(), $body = null, $base_api_path = 'wpcom' ) {
282 $base_api_path = trim( $base_api_path, '/' );
283 $version = ltrim( $version, 'v' );
284 $path = ltrim( $path, '/' );
285
286 $args = array_intersect_key( $args, array(
287 'headers' => 'array',
288 'method' => 'string',
289 'timeout' => 'int',
290 'redirection' => 'int',
291 'stream' => 'boolean',
292 'filename' => 'string',
293 'sslverify' => 'boolean',
294 ) );
295
296 $args['user_id'] = get_current_user_id();
297 $args['method'] = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET';
298 $args['url'] = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $path );
299
300 if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) {
301 $args['headers'] = array( 'Content-Type' => 'application/json' );
302 }
303
304 if ( isset( $body ) && ! is_string( $body ) ) {
305 $body = wp_json_encode( $body );
306 }
307
308 return self::remote_request( $args, $body );
309 }
310
311 /**
312 * Query the WordPress.com REST API using the blog token
313 *
314 * @param string $path
315 * @param string $version
316 * @param array $args
317 * @param string $body
318 * @param string $base_api_path
319 * @return array|WP_Error $response Data.
320 */
321 static function wpcom_json_api_request_as_blog( $path, $version = self::WPCOM_JSON_API_VERSION, $args = array(), $body = null, $base_api_path = 'rest' ) {
322 $filtered_args = array_intersect_key( $args, array(
323 'headers' => 'array',
324 'method' => 'string',
325 'timeout' => 'int',
326 'redirection' => 'int',
327 'stream' => 'boolean',
328 'filename' => 'string',
329 'sslverify' => 'boolean',
330 ) );
331
332 // unprecedingslashit
333 $_path = preg_replace( '/^\//', '', $path );
334
335 // Use GET by default whereas `remote_request` uses POST
336 $request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET';
337
338 $url = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $_path );
339
340 $validated_args = array_merge( $filtered_args, array(
341 'url' => $url,
342 'blog_id' => (int) Jetpack_Options::get_option( 'id' ),
343 'method' => $request_method,
344 ) );
345
346 return Jetpack_Client::remote_request( $validated_args, $body );
347 }
348
349 /**
350 * Takes an array or similar structure and recursively turns all values into strings. This is used to
351 * make sure that body hashes are made ith the string version, which is what will be seen after a
352 * server pulls up the data in the $_POST array.
353 *
354 * @param array|mixed $data
355 *
356 * @return array|string
357 */
358 public static function _stringify_data( $data ) {
359
360 // Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string.
361 if ( is_bool( $data ) ) {
362 return $data ? "1" : "0";
363 }
364
365 // Cast objects into arrays.
366 if ( is_object( $data ) ) {
367 $data = (array) $data;
368 }
369
370 // Non arrays at this point should be just converted to strings.
371 if ( ! is_array( $data ) ) {
372 return (string)$data;
373 }
374
375 foreach ( $data as $key => &$value ) {
376 $value = self::_stringify_data( $value );
377 }
378
379 return $data;
380 }
381
382 /**
383 * Gets protocol string.
384 *
385 * @return string `https` (if possible), else `http`.
386 */
387 public static function protocol() {
388 /**
389 * Determines whether Jetpack can send outbound https requests to the WPCOM api.
390 *
391 * @since 3.6.0
392 *
393 * @param bool $proto Defaults to true.
394 */
395 $https = apply_filters( 'jetpack_can_make_outbound_https', true );
396
397 return $https ? 'https' : 'http';
398 }
399 }
400