Completed
Push — update/videopress-transcode-al... ( ffe79d...2d3973 )
by Kirk
13:06 queued 05:54
created

Jetpack_Signature::join_with_equal_sign()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
/**
3
 * The Jetpack Connection signature class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
9
10
/**
11
 * The Jetpack Connection signature class that is used to sign requests.
12
 */
13
class Jetpack_Signature {
14
	/**
15
	 * Token part of the access token.
16
	 *
17
	 * @access public
18
	 * @var string
19
	 */
20
	public $token;
21
22
	/**
23
	 * Access token secret.
24
	 *
25
	 * @access public
26
	 * @var string
27
	 */
28
	public $secret;
29
30
	/**
31
	 * The current request URL.
32
	 *
33
	 * @access public
34
	 * @var string
35
	 */
36
	public $current_request_url;
37
38
	/**
39
	 * Constructor.
40
	 *
41
	 * @param array $access_token Access token.
42
	 * @param int   $time_diff    Timezone difference (in seconds).
43
	 */
44
	public function __construct( $access_token, $time_diff = 0 ) {
45
		$secret = explode( '.', $access_token );
46
		if ( 2 !== count( $secret ) ) {
47
			return;
48
		}
49
50
		$this->token     = $secret[0];
51
		$this->secret    = $secret[1];
52
		$this->time_diff = $time_diff;
0 ignored issues
show
Bug introduced by
The property time_diff does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53
	}
54
55
	/**
56
	 * Sign the current request.
57
	 *
58
	 * @todo Implement a proper nonce verification.
59
	 *
60
	 * @param array $override Optional arguments to override the ones from the current request.
61
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
62
	 */
63
	public function sign_current_request( $override = array() ) {
64
		if ( isset( $override['scheme'] ) ) {
65
			$scheme = $override['scheme'];
66
			if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
67
				return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_scheme'.

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...
68
			}
69
		} else {
70
			if ( is_ssl() ) {
71
				$scheme = 'https';
72
			} else {
73
				$scheme = 'http';
74
			}
75
		}
76
77
		$host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : $_SERVER['SERVER_PORT'];
78
		$host_port = intval( $host_port );
79
80
		/**
81
		 * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test.
82
		 * Please update the test if any changes are made in this logic.
83
		 */
84
		if ( is_ssl() ) {
85
			// 443: Standard Port
86
			// 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites
87
			// with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with
88
			// the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a
89
			// site at https://example.com:80/ (which would be a strange configuration).
90
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
91
			// if the site is behind a proxy running on port 443 without
92
			// X-Forwarded-Port and the back end's port is *not* 80. It's better,
93
			// though, to configure the proxy to send X-Forwarded-Port.
94
			$https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? JETPACK_SIGNATURE__HTTPS_PORT : 443;
95
			$port       = in_array( $host_port, array( 443, 80, $https_port ), true ) ? '' : $host_port;
96
		} else {
97
			// 80: Standard Port
98
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
99
			// if the site is behind a proxy running on port 80 without
100
			// X-Forwarded-Port. It's better, though, to configure the proxy to
101
			// send X-Forwarded-Port.
102
			$http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? JETPACK_SIGNATURE__HTTP_PORT : 80;
103
			$port      = in_array( $host_port, array( 80, $http_port ), true ) ? '' : $host_port;
104
		}
105
106
		$this->current_request_url = "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . stripslashes( $_SERVER['REQUEST_URI'] );
107
108
		if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) {
109
			$body = $override['body'];
110
		} elseif ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
111
			$body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null;
112
113
			// Convert the $_POST to the body, if the body was empty. This is how arrays are hashed
114
			// and encoded on the Jetpack side.
115
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
116
				// phpcs:ignore WordPress.Security.NonceVerification.Missing
117
				if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) {
118
					$body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
119
				}
120
			}
121
		} elseif ( 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
122
			// This is a little strange-looking, but there doesn't seem to be another way to get the PUT body.
123
			$raw_put_data = file_get_contents( 'php://input' );
124
			parse_str( $raw_put_data, $body );
125
126
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
127
				$put_data = json_decode( $raw_put_data, true );
128
				if ( is_array( $put_data ) && count( $put_data ) > 0 ) {
129
					$body = $put_data;
130
				}
131
			}
132
		} else {
133
			$body = null;
134
		}
135
136
		if ( empty( $body ) ) {
137
			$body = null;
138
		}
139
140
		$a = array();
141
		foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) {
142
			if ( isset( $override[ $parameter ] ) ) {
143
				$a[ $parameter ] = $override[ $parameter ];
144
			} else {
145
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
146
				$a[ $parameter ] = isset( $_GET[ $parameter ] ) ? stripslashes( $_GET[ $parameter ] ) : '';
147
			}
148
		}
149
150
		$method = isset( $override['method'] ) ? $override['method'] : $_SERVER['REQUEST_METHOD'];
151
		return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true );
152
	}
153
154
	/**
155
	 * Sign a specified request.
156
	 *
157
	 * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array?
158
	 * @todo Use wp_json_encode() instead of json_encode()?
159
	 * @todo Use wp_parse_url() instead of parse_url()?
160
	 *
161
	 * @param string $token            Request token.
162
	 * @param int    $timestamp        Timestamp of the request.
163
	 * @param string $nonce            Request nonce.
164
	 * @param string $body_hash        Request body hash.
165
	 * @param string $method           Request method.
166
	 * @param string $url              Request URL.
167
	 * @param mixed  $body             Request body.
168
	 * @param bool   $verify_body_hash Whether to verify the body hash against the body.
169
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
170
	 */
171
	public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) {
172
		if ( ! $this->secret ) {
173
			return new WP_Error( 'invalid_secret', 'Invalid secret' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_secret'.

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...
174
		}
175
176
		if ( ! $this->token ) {
177
			return new WP_Error( 'invalid_token', 'Invalid token' );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_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...
178
		}
179
180
		list( $token ) = explode( '.', $token );
181
182
		$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );
183
184
		if ( 0 !== strpos( $token, "$this->token:" ) ) {
185
			return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'token_mismatch'.

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...
186
		}
187
188
		// If we got an array at this point, let's encode it, so we can see what it looks like as a string.
189
		if ( is_array( $body ) ) {
190
			if ( count( $body ) > 0 ) {
191
				// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
192
				$body = json_encode( $body );
193
194
			} else {
195
				$body = '';
196
			}
197
		}
198
199
		$required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' );
200
		if ( ! is_null( $body ) ) {
201
			$required_parameters[] = 'body_hash';
202
			if ( ! is_string( $body ) ) {
203
				return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) );
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...
204
			}
205
		}
206
207
		foreach ( $required_parameters as $required ) {
208
			if ( ! is_scalar( $$required ) ) {
209
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
210
			}
211
212 View Code Duplication
			if ( ! strlen( $$required ) ) {
213
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
214
			}
215
		}
216
217
		if ( empty( $body ) ) {
218
			if ( $body_hash ) {
219
				return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body_hash'.

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...
220
			}
221
		} else {
222
			$connection = new Connection_Manager();
223
			if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) {
224
				return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_body_hash'.

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...
225
			}
226
		}
227
228
		// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
229
		$parsed = parse_url( $url );
230
		if ( ! isset( $parsed['host'] ) ) {
231
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
232
		}
233
234
		if ( ! empty( $parsed['port'] ) ) {
235
			$port = $parsed['port'];
236
		} else {
237
			if ( 'http' === $parsed['scheme'] ) {
238
				$port = 80;
239
			} elseif ( 'https' === $parsed['scheme'] ) {
240
				$port = 443;
241
			} else {
242
				return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'unknown_scheme_port'.

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...
243
			}
244
		}
245
246 View Code Duplication
		if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug.
247
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
248
		}
249
250
		$local_time = $timestamp - $this->time_diff;
251 View Code Duplication
		if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
252
			return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
253
		}
254
255 View Code Duplication
		if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) {
256
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_signature'.

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...
257
		}
258
259
		$normalized_request_pieces = array(
260
			$token,
261
			$timestamp,
262
			$nonce,
263
			$body_hash,
264
			strtoupper( $method ),
265
			strtolower( $parsed['host'] ),
266
			$port,
267
			$parsed['path'],
268
			// Normalized Query String.
269
		);
270
271
		$normalized_request_pieces      = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) );
272
		$flat_normalized_request_pieces = array();
273
		foreach ( $normalized_request_pieces as $piece ) {
274
			if ( is_array( $piece ) ) {
275
				foreach ( $piece as $subpiece ) {
276
					$flat_normalized_request_pieces[] = $subpiece;
277
				}
278
			} else {
279
				$flat_normalized_request_pieces[] = $piece;
280
			}
281
		}
282
		$normalized_request_pieces = $flat_normalized_request_pieces;
283
284
		$normalized_request_string = join( "\n", $normalized_request_pieces ) . "\n";
285
286
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
287
		return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) );
288
	}
289
290
	/**
291
	 * Retrieve and normalize the parameters from a query string.
292
	 *
293
	 * @param string $query_string Query string.
294
	 * @return array Normalized query string parameters.
295
	 */
296
	public function normalized_query_parameters( $query_string ) {
297
		parse_str( $query_string, $array );
298
		if ( get_magic_quotes_gpc() ) {
299
			$array = stripslashes_deep( $array );
300
		}
301
302
		unset( $array['signature'] );
303
304
		$names  = array_keys( $array );
305
		$values = array_values( $array );
306
307
		$names  = array_map( array( $this, 'encode_3986' ), $names );
308
		$values = array_map( array( $this, 'encode_3986' ), $values );
309
310
		$pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values );
311
312
		sort( $pairs );
313
314
		return $pairs;
315
	}
316
317
	/**
318
	 * Encodes a string or array of strings according to RFC 3986.
319
	 *
320
	 * @param string|array $string_or_array String or array to encode.
321
	 * @return string|array URL-encoded string or array.
322
	 */
323
	public function encode_3986( $string_or_array ) {
324
		if ( is_array( $string_or_array ) ) {
325
			return array_map( array( $this, 'encode_3986' ), $string_or_array );
326
		}
327
328
		return rawurlencode( $string_or_array );
329
	}
330
331
	/**
332
	 * Concatenates a parameter name and a parameter value with an equals sign between them.
333
	 * Supports one-dimensional arrays as `$value`.
334
	 *
335
	 * @param string $name  Parameter name.
336
	 * @param mixed  $value Parameter value.
337
	 * @return string A pair with parameter name and value (e.g. `name=value`).
338
	 */
339
	public function join_with_equal_sign( $name, $value ) {
340
		if ( is_array( $value ) ) {
341
			$result = array();
342
			foreach ( $value as $array_key => $array_value ) {
343
				$result[] = $name . '[' . $array_key . ']=' . $array_value;
344
			}
345
			return $result;
346
		}
347
		return "{$name}={$value}";
348
	}
349
}
350