Completed
Push — move-jetpack-client-to-connect... ( 762fb0 )
by
unknown
08:24
created

Client   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 396
Duplicated Lines 100 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 396
loc 396
rs 5.04
c 0
b 0
f 0
wmc 57
lcom 1
cbo 4

7 Methods

Rating   Name   Duplication   Size   Complexity  
F remote_request() 131 131 16
C _wp_remote_request() 73 73 14
B set_time_diff() 27 27 10
B wpcom_json_api_request_as_user() 32 32 7
A wpcom_json_api_request_as_blog() 33 33 2
B _stringify_data() 23 23 6
A protocol() 12 12 2

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
namespace Automattic\Jetpack\Connection;
4
5 View Code Duplication
class Client {
0 ignored issues
show
Duplication introduced by
This class seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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