Completed
Push — try/namespacing-all-the-things ( 457764 )
by
unknown
08:24
created

class.jetpack-client.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
use Automattic\Jetpack\Jetpack_Data;
3
4
class Jetpack_Client {
5
	const WPCOM_JSON_API_VERSION = '1.1';
6
7
	/**
8
	 * Makes an authorized remote request using Jetpack_Signature
9
	 *
10
	 * @return array|WP_Error WP HTTP response on success
11
	 */
12
	public static function remote_request( $args, $body = null ) {
13
		$defaults = array(
14
			'url' => '',
15
			'user_id' => 0,
16
			'blog_id' => 0,
17
			'auth_location' => JETPACK_CLIENT__AUTH_LOCATION,
18
			'method' => 'POST',
19
			'timeout' => 10,
20
			'redirection' => 0,
21
			'headers' => array(),
22
			'stream' => false,
23
			'filename' => null,
24
			'sslverify' => true,
25
		);
26
27
		$args = wp_parse_args( $args, $defaults );
28
29
		$args['blog_id'] = (int) $args['blog_id'];
30
31
		if ( 'header' != $args['auth_location'] ) {
32
			$args['auth_location'] = 'query_string';
33
		}
34
35
		$token = Jetpack_Data::get_access_token( $args['user_id'] );
0 ignored issues
show
$args['user_id'] is of type integer|string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Deprecated Code introduced by
The method Automattic\Jetpack\Jetpa...ata::get_access_token() has been deprecated with message: 7.5 Use Connection_Manager instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
36
		if ( !$token ) {
37
			return new Jetpack_Error( 'missing_token' );
38
		}
39
40
		$method = strtoupper( $args['method'] );
41
42
		$timeout = intval( $args['timeout'] );
43
44
		$redirection = $args['redirection'];
45
		$stream = $args['stream'];
46
		$filename = $args['filename'];
47
		$sslverify = $args['sslverify'];
48
49
		$request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
50
51
		@list( $token_key, $secret ) = explode( '.', $token->secret );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
52
		if ( empty( $token ) || empty( $secret ) ) {
53
			return new Jetpack_Error( 'malformed_token' );
54
		}
55
56
		$token_key = sprintf( '%s:%d:%d', $token_key, JETPACK__API_VERSION, $token->external_user_id );
57
58
		$time_diff = (int) Jetpack_Options::get_option( 'time_diff' );
59
		$jetpack_signature = new Jetpack_Signature( $token->secret, $time_diff );
60
61
		$timestamp = time() + $time_diff;
62
63
		if( function_exists( 'wp_generate_password' ) ) {
64
			$nonce = wp_generate_password( 10, false );
65
		} else {
66
			$nonce = substr( sha1( rand( 0, 1000000 ) ), 0, 10);
67
		}
68
69
		// Kind of annoying.  Maybe refactor Jetpack_Signature to handle body-hashing
70
		if ( is_null( $body ) ) {
71
			$body_hash = '';
72
73
		} else {
74
			// Allow arrays to be used in passing data.
75
			$body_to_hash = $body;
76
77
			if ( is_array( $body ) ) {
78
				// We cast this to a new variable, because the array form of $body needs to be
79
				// maintained so it can be passed into the request later on in the code.
80
				if ( count( $body ) > 0 ) {
81
					$body_to_hash = json_encode( self::_stringify_data( $body ) );
82
				} else {
83
					$body_to_hash = '';
84
				}
85
			}
86
87
			if ( ! is_string( $body_to_hash ) ) {
88
				return new Jetpack_Error( 'invalid_body', 'Body is malformed.' );
89
			}
90
91
			$body_hash = Jetpack::connection()->sha1_base64( $body_to_hash );
92
		}
93
94
		$auth = array(
95
			'token' => $token_key,
96
			'timestamp' => $timestamp,
97
			'nonce' => $nonce,
98
			'body-hash' => $body_hash,
99
		);
100
101
		if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) {
102
			$url_args = array(
103
				'for'           => 'jetpack',
104
				'wpcom_blog_id' => Jetpack_Options::get_option( 'id' ),
105
			);
106
		} else {
107
			$url_args = array();
108
		}
109
110
		if ( 'header' != $args['auth_location'] ) {
111
			$url_args += $auth;
112
		}
113
114
		$url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
115
		$url = Jetpack::fix_url_for_bad_hosts( $url );
116
117
		$signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
118
119
		if ( !$signature || is_wp_error( $signature ) ) {
120
			return $signature;
121
		}
122
123
		// Send an Authorization header so various caches/proxies do the right thing
124
		$auth['signature'] = $signature;
125
		$auth['version'] = JETPACK__VERSION;
126
		$header_pieces = array();
127
		foreach ( $auth as $key => $value ) {
128
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
129
		}
130
		$request['headers'] = array_merge( $args['headers'], array(
131
			'Authorization' => "X_JETPACK " . join( ' ', $header_pieces ),
132
		) );
133
134
		if ( 'header' != $args['auth_location'] ) {
135
			$url = add_query_arg( 'signature', urlencode( $signature ), $url );
136
		}
137
138
		return Jetpack_Client::_wp_remote_request( $url, $request );
139
	}
140
141
	/**
142
	 * Wrapper for wp_remote_request().  Turns off SSL verification for certain SSL errors.
143
	 * This is lame, but many, many, many hosts have misconfigured SSL.
144
	 *
145
	 * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if:
146
	 * 1. a certificate error is found AND
147
	 * 2. not verifying the certificate works around the problem.
148
	 *
149
	 * The option is checked on each request.
150
	 *
151
	 * @internal
152
	 * @see Jetpack::fix_url_for_bad_hosts()
153
	 *
154
	 * @return array|WP_Error WP HTTP response on success
155
	 */
156
	public static function _wp_remote_request( $url, $args, $set_fallback = false ) {
157
		/**
158
		 * SSL verification (`sslverify`) for the JetpackClient remote request
159
		 * defaults to off, use this filter to force it on.
160
		 *
161
		 * Return `true` to ENABLE SSL verification, return `false`
162
		 * to DISABLE SSL verification.
163
		 *
164
		 * @since 3.6.0
165
		 *
166
		 * @param bool Whether to force `sslverify` or not.
167
		 */
168
		if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) {
169
			return wp_remote_request( $url, $args );
170
		}
171
172
		$fallback = Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
173
		if ( false === $fallback ) {
174
			Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 );
175
		}
176
177
		if ( (int) $fallback ) {
178
			// We're flagged to fallback
179
			$args['sslverify'] = false;
180
		}
181
182
		$response = wp_remote_request( $url, $args );
183
184
		if (
185
			!$set_fallback                                     // We're not allowed to set the flag on this request, so whatever happens happens
186
		||
187
			isset( $args['sslverify'] ) && !$args['sslverify'] // No verification - no point in doing it again
188
		||
189
			!is_wp_error( $response )                          // Let it ride
190
		) {
191
			Jetpack_Client::set_time_diff( $response, $set_fallback );
192
			return $response;
193
		}
194
195
		// At this point, we're not flagged to fallback and we are allowed to set the flag on this request.
196
197
		$message = $response->get_error_message();
198
199
		// Is it an SSL Certificate verification error?
200
		if (
201
			false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error
202
		&&
203
			false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error
204
		&&
205
			false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found
206
		&&
207
			false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful
208
			                                                                              // different versions of curl have different error messages
209
			                                                                              // this string should catch them all
210
		&&
211
			false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights
212
		) {
213
			// No, it is not.
214
			return $response;
215
		}
216
217
		// Redo the request without SSL certificate verification.
218
		$args['sslverify'] = false;
219
		$response = wp_remote_request( $url, $args );
220
221
		if ( !is_wp_error( $response ) ) {
222
			// The request went through this time, flag for future fallbacks
223
			Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() );
224
			Jetpack_Client::set_time_diff( $response, $set_fallback );
225
		}
226
227
		return $response;
228
	}
229
230
	public static function set_time_diff( &$response, $force_set = false ) {
231
		$code = wp_remote_retrieve_response_code( $response );
232
233
		// Only trust the Date header on some responses
234
		if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) {
235
			return;
236
		}
237
238
		if ( !$date = wp_remote_retrieve_header( $response, 'date' ) ) {
239
			return;
240
		}
241
242
		if ( 0 >= $time = (int) strtotime( $date ) ) {
243
			return;
244
		}
245
246
		$time_diff = $time - time();
247
248
		if ( $force_set ) { // during register
249
			Jetpack_Options::update_option( 'time_diff', $time_diff );
250
		} else { // otherwise
251
			$old_diff = Jetpack_Options::get_option( 'time_diff' );
252
			if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) {
253
				Jetpack_Options::update_option( 'time_diff', $time_diff );
254
			}
255
		}
256
	}
257
258
	/**
259
	 * Queries the WordPress.com REST API with a user token.
260
	 *
261
	 * @param  string $path             REST API path.
262
	 * @param  string $version          REST API version. Default is `2`.
263
	 * @param  array  $args             Arguments to {@see WP_Http}. Default is `array()`.
264
	 * @param  string $body             Body passed to {@see WP_Http}. Default is `null`.
0 ignored issues
show
Should the type for parameter $body not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
265
	 * @param  string $base_api_path    REST API root. Default is `wpcom`.
266
	 *
267
	 * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
268
	 */
269
	public static function wpcom_json_api_request_as_user( $path, $version = '2', $args = array(), $body = null, $base_api_path = 'wpcom' ) {
270
		$base_api_path = trim( $base_api_path, '/' );
271
		$version       = ltrim( $version, 'v' );
272
		$path          = ltrim( $path, '/' );
273
274
		$args = array_intersect_key( $args, array(
275
			'headers'     => 'array',
276
			'method'      => 'string',
277
			'timeout'     => 'int',
278
			'redirection' => 'int',
279
			'stream'      => 'boolean',
280
			'filename'    => 'string',
281
			'sslverify'   => 'boolean',
282
		) );
283
284
		$args['user_id'] = get_current_user_id();
285
		$args['method']  = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET';
286
		$args['url']     = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $path );
287
288
		if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) {
289
			$args['headers'] = array( 'Content-Type' => 'application/json' );
290
		}
291
292
		if ( isset( $body ) && ! is_string( $body ) ) {
293
			$body = wp_json_encode( $body );
294
		}
295
296
		return self::remote_request( $args, $body );
297
	}
298
299
	/**
300
	 * Query the WordPress.com REST API using the blog token
301
	 *
302
	 * @param string  $path
303
	 * @param string  $version
304
	 * @param array   $args
305
	 * @param string  $body
0 ignored issues
show
Should the type for parameter $body not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
306
	 * @param string  $base_api_path
307
	 * @return array|WP_Error $response Data.
308
	 */
309
	static function wpcom_json_api_request_as_blog( $path, $version = self::WPCOM_JSON_API_VERSION, $args = array(), $body = null, $base_api_path = 'rest' ) {
310
		$filtered_args = array_intersect_key( $args, array(
311
			'headers'     => 'array',
312
			'method'      => 'string',
313
			'timeout'     => 'int',
314
			'redirection' => 'int',
315
			'stream'      => 'boolean',
316
			'filename'    => 'string',
317
			'sslverify'   => 'boolean',
318
		) );
319
320
		// unprecedingslashit
321
		$_path = preg_replace( '/^\//', '', $path );
322
323
		// Use GET by default whereas `remote_request` uses POST
324
		$request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET';
325
326
		$url = sprintf( '%s://%s/%s/v%s/%s', self::protocol(), JETPACK__WPCOM_JSON_API_HOST, $base_api_path, $version, $_path );
327
328
		$validated_args = array_merge( $filtered_args, array(
329
			'url'     => $url,
330
			'blog_id' => (int) Jetpack_Options::get_option( 'id' ),
331
			'method'  => $request_method,
332
		) );
333
334
		return Jetpack_Client::remote_request( $validated_args, $body );
335
	}
336
337
	/**
338
	 * Takes an array or similar structure and recursively turns all values into strings. This is used to
339
	 * make sure that body hashes are made ith the string version, which is what will be seen after a
340
	 * server pulls up the data in the $_POST array.
341
	 *
342
	 * @param array|mixed $data
343
	 *
344
	 * @return array|string
345
	 */
346
	public static function _stringify_data( $data ) {
347
348
		// Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string.
349
		if ( is_bool( $data ) ) {
350
			return $data ? "1" : "0";
351
		}
352
353
		// Cast objects into arrays.
354
		if ( is_object( $data ) ) {
355
			$data = (array) $data;
356
		}
357
358
		// Non arrays at this point should be just converted to strings.
359
		if ( ! is_array( $data ) ) {
360
			return (string)$data;
361
		}
362
363
		foreach ( $data as $key => &$value ) {
364
			$value = self::_stringify_data( $value );
365
		}
366
367
		return $data;
368
	}
369
370
	/**
371
	 * Gets protocol string.
372
	 *
373
	 * @return string `https` (if possible), else `http`.
374
	 */
375
	public static function protocol() {
376
		/**
377
		 * Determines whether Jetpack can send outbound https requests to the WPCOM api.
378
		 *
379
		 * @since 3.6.0
380
		 *
381
		 * @param bool $proto Defaults to true.
382
		 */
383
		$https = apply_filters( 'jetpack_can_make_outbound_https', true );
384
385
		return $https ? 'https' : 'http';
386
	}
387
}
388