Completed
Push — add/e2e-connection-purchase-fl... ( d02cce...68beb5 )
by Yaroslav
14:07 queued 04:35
created

Client   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 450
Duplicated Lines 2 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 9
loc 450
rs 4.08
c 0
b 0
f 0
wmc 59
lcom 1
cbo 6

7 Methods

Rating   Name   Duplication   Size   Complexity  
B _stringify_data() 0 23 6
A protocol() 0 12 2
F remote_request() 5 139 16
C _wp_remote_request() 0 73 14
B set_time_diff() 0 29 10
B wpcom_json_api_request_as_user() 0 45 7
A wpcom_json_api_request_as_blog() 4 53 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * The Connection Client class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
namespace Automattic\Jetpack\Connection;
9
10
use Automattic\Jetpack\Constants;
11
12
/**
13
 * The Client class that is used to connect to WordPress.com Jetpack API.
14
 */
15
class Client {
16
	const WPCOM_JSON_API_VERSION = '1.1';
17
18
	/**
19
	 * Makes an authorized remote request using Jetpack_Signature
20
	 *
21
	 * @param array        $args the arguments for the remote request.
22
	 * @param array|String $body the request body.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $body not be array|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...
23
	 * @return array|WP_Error WP HTTP response on success
24
	 */
25
	public static function remote_request( $args, $body = null ) {
26
		Utils::init_default_constants();
27
28
		$defaults = array(
29
			'url'           => '',
30
			'user_id'       => 0,
31
			'blog_id'       => 0,
32
			'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ),
33
			'method'        => 'POST',
34
			'timeout'       => 10,
35
			'redirection'   => 0,
36
			'headers'       => array(),
37
			'stream'        => false,
38
			'filename'      => null,
39
			'sslverify'     => true,
40
		);
41
42
		$args = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,*,{"url":"s..."sslverify":"boolean"}>, but the function expects a string.

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...
43
44
		$args['blog_id'] = (int) $args['blog_id'];
45
46
		if ( 'header' !== $args['auth_location'] ) {
47
			$args['auth_location'] = 'query_string';
48
		}
49
50
		$connection = new Manager();
51
		$token      = $connection->get_access_token( $args['user_id'] );
52
		if ( ! $token ) {
53
			return new \WP_Error( 'missing_token' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'missing_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
54
		}
55
56
		$method = strtoupper( $args['method'] );
57
58
		$timeout = intval( $args['timeout'] );
59
60
		$redirection = $args['redirection'];
61
		$stream      = $args['stream'];
62
		$filename    = $args['filename'];
63
		$sslverify   = $args['sslverify'];
64
65
		$request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
66
67
		@list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
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...
68
		if ( empty( $token ) || empty( $secret ) ) {
69
			return new \WP_Error( 'malformed_token' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'malformed_token'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
70
		}
71
72
		$token_key = sprintf(
73
			'%s:%d:%d',
74
			$token_key,
75
			Constants::get_constant( 'JETPACK__API_VERSION' ),
76
			$token->external_user_id
77
		);
78
79
		$time_diff         = (int) \Jetpack_Options::get_option( 'time_diff' );
80
		$jetpack_signature = new \Jetpack_Signature( $token->secret, $time_diff );
81
82
		$timestamp = time() + $time_diff;
83
84 View Code Duplication
		if ( function_exists( 'wp_generate_password' ) ) {
85
			$nonce = wp_generate_password( 10, false );
86
		} else {
87
			$nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
88
		}
89
90
		// Kind of annoying.  Maybe refactor Jetpack_Signature to handle body-hashing.
91
		if ( is_null( $body ) ) {
92
			$body_hash = '';
93
94
		} else {
95
			// Allow arrays to be used in passing data.
96
			$body_to_hash = $body;
97
98
			if ( is_array( $body ) ) {
99
				// We cast this to a new variable, because the array form of $body needs to be
100
				// maintained so it can be passed into the request later on in the code.
101
				if ( count( $body ) > 0 ) {
102
					$body_to_hash = wp_json_encode( self::_stringify_data( $body ) );
103
				} else {
104
					$body_to_hash = '';
105
				}
106
			}
107
108
			if ( ! is_string( $body_to_hash ) ) {
109
				return new \WP_Error( 'invalid_body', 'Body is malformed.' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
110
			}
111
112
			$body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
113
		}
114
115
		$auth = array(
116
			'token'     => $token_key,
117
			'timestamp' => $timestamp,
118
			'nonce'     => $nonce,
119
			'body-hash' => $body_hash,
120
		);
121
122
		if ( false !== strpos( $args['url'], 'xmlrpc.php' ) ) {
123
			$url_args = array(
124
				'for'           => 'jetpack',
125
				'wpcom_blog_id' => \Jetpack_Options::get_option( 'id' ),
126
			);
127
		} else {
128
			$url_args = array();
129
		}
130
131
		if ( 'header' !== $args['auth_location'] ) {
132
			$url_args += $auth;
133
		}
134
135
		$url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
136
		$url = Utils::fix_url_for_bad_hosts( $url );
137
138
		$signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
139
140
		if ( ! $signature || is_wp_error( $signature ) ) {
141
			return $signature;
142
		}
143
144
		// Send an Authorization header so various caches/proxies do the right thing.
145
		$auth['signature'] = $signature;
146
		$auth['version']   = Constants::get_constant( 'JETPACK__VERSION' );
147
		$header_pieces     = array();
148
		foreach ( $auth as $key => $value ) {
149
			$header_pieces[] = sprintf( '%s="%s"', $key, $value );
150
		}
151
		$request['headers'] = array_merge(
152
			$args['headers'],
153
			array(
154
				'Authorization' => 'X_JETPACK ' . join( ' ', $header_pieces ),
155
			)
156
		);
157
158
		if ( 'header' !== $args['auth_location'] ) {
159
			$url = add_query_arg( 'signature', rawurlencode( $signature ), $url );
160
		}
161
162
		return self::_wp_remote_request( $url, $request );
163
	}
164
165
	/**
166
	 * Wrapper for wp_remote_request().  Turns off SSL verification for certain SSL errors.
167
	 * This is lame, but many, many, many hosts have misconfigured SSL.
168
	 *
169
	 * When Jetpack is registered, the jetpack_fallback_no_verify_ssl_certs option is set to the current time if:
170
	 * 1. a certificate error is found AND
171
	 * 2. not verifying the certificate works around the problem.
172
	 *
173
	 * The option is checked on each request.
174
	 *
175
	 * @internal
176
	 * @see Utils::fix_url_for_bad_hosts()
177
	 *
178
	 * @param String  $url the request URL.
179
	 * @param array   $args request arguments.
180
	 * @param Boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
181
	 * @return array|WP_Error WP HTTP response on success
182
	 */
183
	public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
184
		/**
185
		 * SSL verification (`sslverify`) for the JetpackClient remote request
186
		 * defaults to off, use this filter to force it on.
187
		 *
188
		 * Return `true` to ENABLE SSL verification, return `false`
189
		 * to DISABLE SSL verification.
190
		 *
191
		 * @since 3.6.0
192
		 *
193
		 * @param bool Whether to force `sslverify` or not.
194
		 */
195
		if ( apply_filters( 'jetpack_client_verify_ssl_certs', false ) ) {
196
			return wp_remote_request( $url, $args );
197
		}
198
199
		$fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
200
		if ( false === $fallback ) {
201
			\Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', 0 );
202
		}
203
204
		if ( (int) $fallback ) {
205
			// We're flagged to fallback.
206
			$args['sslverify'] = false;
207
		}
208
209
		$response = wp_remote_request( $url, $args );
210
211
		if (
212
			! $set_fallback                                     // We're not allowed to set the flag on this request, so whatever happens happens.
213
			||
214
			isset( $args['sslverify'] ) && ! $args['sslverify'] // No verification - no point in doing it again.
215
			||
216
			! is_wp_error( $response )                          // Let it ride.
217
		) {
218
			self::set_time_diff( $response, $set_fallback );
219
			return $response;
220
		}
221
222
		// At this point, we're not flagged to fallback and we are allowed to set the flag on this request.
223
224
		$message = $response->get_error_message();
225
226
		// Is it an SSL Certificate verification error?
227
		if (
228
			false === strpos( $message, '14090086' ) // OpenSSL SSL3 certificate error.
229
			&&
230
			false === strpos( $message, '1407E086' ) // OpenSSL SSL2 certificate error.
231
			&&
232
			false === strpos( $message, 'error setting certificate verify locations' ) // cURL CA bundle not found.
233
			&&
234
			false === strpos( $message, 'Peer certificate cannot be authenticated with' ) // cURL CURLE_SSL_CACERT: CA bundle found, but not helpful
235
			// Different versions of curl have different error messages
236
			// this string should catch them all.
237
			&&
238
			false === strpos( $message, 'Problem with the SSL CA cert' ) // cURL CURLE_SSL_CACERT_BADFILE: probably access rights.
239
		) {
240
			// No, it is not.
241
			return $response;
242
		}
243
244
		// Redo the request without SSL certificate verification.
245
		$args['sslverify'] = false;
246
		$response          = wp_remote_request( $url, $args );
247
248
		if ( ! is_wp_error( $response ) ) {
249
			// The request went through this time, flag for future fallbacks.
250
			\Jetpack_Options::update_option( 'fallback_no_verify_ssl_certs', time() );
251
			self::set_time_diff( $response, $set_fallback );
252
		}
253
254
		return $response;
255
	}
256
257
	/**
258
	 * Sets the time difference for correct signature computation.
259
	 *
260
	 * @param HTTP_Response $response the response object.
261
	 * @param Boolean       $force_set whether to force setting the time difference.
262
	 */
263
	public static function set_time_diff( &$response, $force_set = false ) {
264
		$code = wp_remote_retrieve_response_code( $response );
265
266
		// Only trust the Date header on some responses.
267
		if ( 200 != $code && 304 != $code && 400 != $code && 401 != $code ) { // phpcs:ignore  WordPress.PHP.StrictComparisons.LooseComparison
268
			return;
269
		}
270
271
		$date = wp_remote_retrieve_header( $response, 'date' );
272
		if ( ! $date ) {
273
			return;
274
		}
275
276
		$time = (int) strtotime( $date );
277
		if ( 0 >= $time ) {
278
			return;
279
		}
280
281
		$time_diff = $time - time();
282
283
		if ( $force_set ) { // During register.
284
			\Jetpack_Options::update_option( 'time_diff', $time_diff );
285
		} else { // Otherwise.
286
			$old_diff = \Jetpack_Options::get_option( 'time_diff' );
287
			if ( false === $old_diff || abs( $time_diff - (int) $old_diff ) > 10 ) {
288
				\Jetpack_Options::update_option( 'time_diff', $time_diff );
289
			}
290
		}
291
	}
292
293
	/**
294
	 * Queries the WordPress.com REST API with a user token.
295
	 *
296
	 * @param  string $path             REST API path.
297
	 * @param  string $version          REST API version. Default is `2`.
298
	 * @param  array  $args             Arguments to {@see WP_Http}. Default is `array()`.
299
	 * @param  string $body             Body passed to {@see WP_Http}. Default is `null`.
0 ignored issues
show
Documentation introduced by
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...
300
	 * @param  string $base_api_path    REST API root. Default is `wpcom`.
301
	 *
302
	 * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
303
	 */
304
	public static function wpcom_json_api_request_as_user(
305
		$path,
306
		$version = '2',
307
		$args = array(),
308
		$body = null,
309
		$base_api_path = 'wpcom'
310
	) {
311
		$base_api_path = trim( $base_api_path, '/' );
312
		$version       = ltrim( $version, 'v' );
313
		$path          = ltrim( $path, '/' );
314
315
		$args = array_intersect_key(
316
			$args,
317
			array(
318
				'headers'     => 'array',
319
				'method'      => 'string',
320
				'timeout'     => 'int',
321
				'redirection' => 'int',
322
				'stream'      => 'boolean',
323
				'filename'    => 'string',
324
				'sslverify'   => 'boolean',
325
			)
326
		);
327
328
		$args['user_id'] = get_current_user_id();
329
		$args['method']  = isset( $args['method'] ) ? strtoupper( $args['method'] ) : 'GET';
330
		$args['url']     = sprintf(
331
			'%s://%s/%s/v%s/%s',
332
			self::protocol(),
333
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ),
334
			$base_api_path,
335
			$version,
336
			$path
337
		);
338
339
		if ( isset( $body ) && ! isset( $args['headers'] ) && in_array( $args['method'], array( 'POST', 'PUT', 'PATCH' ), true ) ) {
340
			$args['headers'] = array( 'Content-Type' => 'application/json' );
341
		}
342
343
		if ( isset( $body ) && ! is_string( $body ) ) {
344
			$body = wp_json_encode( $body );
345
		}
346
347
		return self::remote_request( $args, $body );
348
	}
349
350
	/**
351
	 * Query the WordPress.com REST API using the blog token
352
	 *
353
	 * @param String $path The API endpoint relative path.
354
	 * @param String $version The API version.
355
	 * @param array  $args Request arguments.
356
	 * @param String $body Request body.
0 ignored issues
show
Documentation introduced by
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...
357
	 * @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
358
	 * @return array|WP_Error $response Data.
359
	 */
360
	public static function wpcom_json_api_request_as_blog(
361
		$path,
362
		$version = self::WPCOM_JSON_API_VERSION,
363
		$args = array(),
364
		$body = null,
365
		$base_api_path = 'rest'
366
	) {
367
368
		// Allow use a store sandbox. Internal ref: PCYsg-IA-p2.
369 View Code Duplication
		if ( isset( $_COOKIE ) && isset( $_COOKIE['store_sandbox'] ) ) {
370
			$secret                    = $_COOKIE['store_sandbox'];
371
			$args['headers']['Cookie'] = "store_sandbox=$secret;";
372
		}
373
374
		$filtered_args = array_intersect_key(
375
			$args,
376
			array(
377
				'headers'     => 'array',
378
				'method'      => 'string',
379
				'timeout'     => 'int',
380
				'redirection' => 'int',
381
				'stream'      => 'boolean',
382
				'filename'    => 'string',
383
				'sslverify'   => 'boolean',
384
			)
385
		);
386
387
		// unprecedingslashit.
388
		$_path = preg_replace( '/^\//', '', $path );
389
390
		// Use GET by default whereas `remote_request` uses POST.
391
		$request_method = ( isset( $filtered_args['method'] ) ) ? $filtered_args['method'] : 'GET';
392
393
		$url = sprintf(
394
			'%s://%s/%s/v%s/%s',
395
			self::protocol(),
396
			Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' ),
397
			$base_api_path,
398
			$version,
399
			$_path
400
		);
401
402
		$validated_args = array_merge(
403
			$filtered_args,
404
			array(
405
				'url'     => $url,
406
				'blog_id' => (int) \Jetpack_Options::get_option( 'id' ),
407
				'method'  => $request_method,
408
			)
409
		);
410
411
		return self::remote_request( $validated_args, $body );
412
	}
413
414
	/**
415
	 * Takes an array or similar structure and recursively turns all values into strings. This is used to
416
	 * make sure that body hashes are made ith the string version, which is what will be seen after a
417
	 * server pulls up the data in the $_POST array.
418
	 *
419
	 * @param array|Mixed $data the data that needs to be stringified.
420
	 *
421
	 * @return array|string
422
	 */
423
	public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
424
425
		// Booleans are special, lets just makes them and explicit 1/0 instead of the 0 being an empty string.
426
		if ( is_bool( $data ) ) {
427
			return $data ? '1' : '0';
428
		}
429
430
		// Cast objects into arrays.
431
		if ( is_object( $data ) ) {
432
			$data = (array) $data;
433
		}
434
435
		// Non arrays at this point should be just converted to strings.
436
		if ( ! is_array( $data ) ) {
437
			return (string) $data;
438
		}
439
440
		foreach ( $data as &$value ) {
441
			$value = self::_stringify_data( $value );
442
		}
443
444
		return $data;
445
	}
446
447
	/**
448
	 * Gets protocol string.
449
	 *
450
	 * @return string `https` (if possible), else `http`.
451
	 */
452
	public static function protocol() {
453
		/**
454
		 * Determines whether Jetpack can send outbound https requests to the WPCOM api.
455
		 *
456
		 * @since 3.6.0
457
		 *
458
		 * @param bool $proto Defaults to true.
459
		 */
460
		$https = apply_filters( 'jetpack_can_make_outbound_https', true );
461
462
		return $https ? 'https' : 'http';
463
	}
464
}
465