PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.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 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-connection/src/class-client.php +90 -37 12.0.3 → 16.3-beta View file →
@@ -7,9 +7,13 @@
7 7
8 8 namespace Automattic\Jetpack\Connection;
9 9
10 10 use Automattic\Jetpack\Constants;
11 +use WP_Error;
11 12
13 +// `wp_remote_request` returns an array with a particular format.
14 +'@phan-type _WP_Remote_Response_Array = array{headers:\WpOrg\Requests\Utility\CaseInsensitiveDictionary,body:string,response:array{code:int,message:string},cookies:\WP_HTTP_Cookie[],filename:?string,http_response:WP_HTTP_Requests_Response}';
15 +
12 16 /**
13 17 * The Client class that is used to connect to WordPress.com Jetpack API.
14 18 */
15 19 class Client {
@@ -17,11 +21,12 @@
17 21
18 22 /**
19 23 * Makes an authorized remote request using Jetpack_Signature
20 24 *
21 - * @param array $args the arguments for the remote request.
22 - * @param array|String $body the request body.
25 + * @param array $args the arguments for the remote request.
26 + * @param array|string|null $body the request body.
23 27 * @return array|WP_Error WP HTTP response on success
28 + * @phan-return _WP_Remote_Response_Array|WP_Error
24 29 */
25 30 public static function remote_request( $args, $body = null ) {
26 31 if ( isset( $args['url'] ) ) {
27 32 /**
@@ -33,10 +38,36 @@
33 38 */
34 39 $args['url'] = apply_filters( 'jetpack_remote_request_url', $args['url'] );
35 40 }
36 41
42 + // Feed failures into the outgoing-request error flow of Error_Handler: any known
43 + // connection error is stored and surfaced to the user. See the Error_Handler
44 + // class docblock for the full picture of both error-handling flows.
45 + // Outgoing XML-RPC calls (Jetpack_IXR_Client) are funneled through this method too;
46 + // tell the transports apart by the endpoint the request targets.
47 + // The literals match Error_Handler::ERROR_TYPE_XMLRPC / ERROR_TYPE_REST. The constants
48 + // themselves must not be referenced from this class: during a plugin update, a stale
49 + // Error_Handler that predates them can already be loaded, and resolving them against
50 + // it would fatal the request.
51 + $request_path = (string) wp_parse_url( empty( $args['url'] ) ? '' : $args['url'], PHP_URL_PATH );
52 + $error_type = '/xmlrpc.php' === substr( $request_path, -strlen( '/xmlrpc.php' ) ) ? 'xmlrpc' : 'rest';
53 +
37 54 $result = self::build_signed_request( $args, $body );
38 - if ( ! $result || is_wp_error( $result ) ) {
55 + if ( is_wp_error( $result ) ) {
56 + // The request was never made, so it has no response to check. Report the signing
57 + // failure; attribution comes from the error itself — see
58 + // `Error_Handler::check_signed_request_for_errors()`.
59 + // Reporting is best-effort and must never fatal a request running mid-plugin-update:
60 + // skip it when the already-loaded Error_Handler is a stale version predating this method.
61 + if ( method_exists( Error_Handler::class, 'check_signed_request_for_errors' ) ) {
62 + Error_Handler::get_instance()->check_signed_request_for_errors(
63 + $result,
64 + empty( $args['url'] ) ? '' : $args['url'],
65 + empty( $args['method'] ) ? 'POST' : $args['method'],
66 + $error_type
67 + );
68 + }
69 +
39 70 return $result;
40 71 }
41 72
42 73 $response = self::_wp_remote_request( $result['url'], $result['request'] );
@@ -45,9 +76,9 @@
45 76 $response,
46 77 $result['auth'],
47 78 empty( $args['url'] ) ? '' : $args['url'],
48 79 empty( $args['method'] ) ? 'POST' : $args['method'],
49 - 'rest'
80 + $error_type
50 81 );
51 82
52 83 /**
53 84 * Fired when the remote request response has been received.
@@ -63,14 +94,14 @@
63 94
64 95 /**
65 96 * Adds authorization signature to a remote request using Jetpack_Signature
66 97 *
67 - * @param array $args the arguments for the remote request.
68 - * @param array|String $body the request body.
69 - * @return WP_Error|array {
98 + * @param array $args the arguments for the remote request.
99 + * @param array|string|null $body the request body.
100 + * @return WP_Error|array{url:string,request:array,auth:array} {
70 101 * An array containing URL and request items.
71 102 *
72 - * @type String $url The request URL.
103 + * @type string $url The request URL.
73 104 * @type array $request Request arguments.
74 105 * @type array $auth Authorization data.
75 106 * }
76 107 */
@@ -87,8 +118,9 @@
87 118 'user_id' => 0,
88 119 'blog_id' => 0,
89 120 'auth_location' => Constants::get_constant( 'JETPACK_CLIENT__AUTH_LOCATION' ),
90 121 'method' => 'POST',
122 + 'format' => 'json',
91 123 'timeout' => 10,
92 124 'redirection' => 0,
93 125 'headers' => array(),
94 126 'stream' => false,
@@ -103,11 +135,22 @@
103 135 if ( 'header' !== $args['auth_location'] ) {
104 136 $args['auth_location'] = 'query_string';
105 137 }
106 138
107 - $token = ( new Tokens() )->get_access_token( $args['user_id'] );
139 + // Return the specific reason the token could not be loaded instead of a bare `false`.
140 + // Note the returned `WP_Error` is truthy, so this must not be tested with `! $token`.
141 + $token = ( new Tokens() )->get_access_token(
142 + $args['user_id'],
143 + false, // token_key
144 + false // suppress_errors
145 + );
146 + if ( is_wp_error( $token ) ) {
147 + return $token;
148 + }
108 149 if ( ! $token ) {
109 - return new \WP_Error( 'missing_token' );
150 + // `get_access_token()` explains itself for every case but one: it returns a bare
151 + // `false` when the tokens are locked. That lock is one-shot and self-healing.
152 + return new WP_Error( 'tokens_locked' );
110 153 }
111 154
112 155 $method = strtoupper( $args['method'] );
113 156
@@ -120,10 +163,10 @@
120 163
121 164 $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
122 165
123 166 @list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
124 - if ( empty( $token ) || empty( $secret ) ) {
125 - return new \WP_Error( 'malformed_token' );
167 + if ( ! $secret ) {
168 + return new WP_Error( 'malformed_token' );
126 169 }
127 170
128 171 $token_key = sprintf(
129 172 '%s:%d:%d',
@@ -139,9 +182,9 @@
139 182
140 183 if ( function_exists( 'wp_generate_password' ) ) {
141 184 $nonce = wp_generate_password( 10, false );
142 185 } else {
143 - $nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
186 + $nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
144 187 }
145 188
146 189 // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing.
147 190 if ( $body === null ) {
@@ -150,13 +193,19 @@
150 193 } else {
151 194 // Allow arrays to be used in passing data.
152 195 $body_to_hash = $body;
153 196
154 - if ( is_array( $body ) ) {
197 + if ( $args['format'] === 'jsonl' ) {
198 + parse_str( $body, $body_to_hash );
199 + }
200 + if ( is_array( $body_to_hash ) ) {
155 201 // We cast this to a new variable, because the array form of $body needs to be
156 202 // maintained so it can be passed into the request later on in the code.
157 - if ( count( $body ) > 0 ) {
158 - $body_to_hash = wp_json_encode( self::_stringify_data( $body ) );
203 + if ( array() !== $body_to_hash ) {
204 + $body_to_hash = wp_json_encode(
205 + self::_stringify_data( $body_to_hash ),
206 + 0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end.
207 + );
159 208 } else {
160 209 $body_to_hash = '';
161 210 }
162 211 }
@@ -161,11 +210,10 @@
161 210 }
162 211 }
163 212
164 213 if ( ! is_string( $body_to_hash ) ) {
165 - return new \WP_Error( 'invalid_body', 'Body is malformed.' );
214 + return new WP_Error( 'invalid_body', 'Body is malformed.' );
166 215 }
167 -
168 216 $body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
169 217 }
170 218
171 219 $auth = array(
@@ -191,9 +239,9 @@
191 239 $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
192 240
193 241 $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
194 242
195 - if ( ! $signature || is_wp_error( $signature ) ) {
243 + if ( is_wp_error( $signature ) ) {
196 244 return $signature;
197 245 }
198 246
199 247 // Send an Authorization header so various caches/proxies do the right thing.
@@ -205,9 +253,9 @@
205 253 }
206 254 $request['headers'] = array_merge(
207 255 $args['headers'],
208 256 array(
209 - 'Authorization' => 'X_JETPACK ' . join( ' ', $header_pieces ),
257 + 'Authorization' => 'X_JETPACK ' . implode( ' ', $header_pieces ),
210 258 )
211 259 );
212 260
213 261 if ( 'header' !== $args['auth_location'] ) {
@@ -228,12 +276,13 @@
228 276 * The option is checked on each request.
229 277 *
230 278 * @internal
231 279 *
232 - * @param String $url the request URL.
280 + * @param string $url the request URL.
233 281 * @param array $args request arguments.
234 - * @param Boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
282 + * @param boolean $set_fallback whether to allow flagging this request to use a fallback certficate override.
235 283 * @return array|WP_Error WP HTTP response on success
284 + * @phan-return _WP_Remote_Response_Array|WP_Error
236 285 */
237 286 public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
238 287 $fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
239 288 if ( false === $fallback ) {
@@ -311,10 +360,11 @@
311 360
312 361 /**
313 362 * Sets the time difference for correct signature computation.
314 363 *
315 - * @param HTTP_Response $response the response object.
316 - * @param Boolean $force_set whether to force setting the time difference.
364 + * @param array|WP_Error $response Response array from `wp_remote_request`, or WP_Error on error.
365 + * @param bool $force_set whether to force setting the time difference.
366 + * @phan-param _WP_Remote_Response_Array|WP_Error $response
317 367 */
318 368 public static function set_time_diff( &$response, $force_set = false ) {
319 369 $code = wp_remote_retrieve_response_code( $response );
320 370
@@ -352,9 +402,9 @@
352 402 * @param string $version REST API version. Default is `2`.
353 403 * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
354 404 * @param string $base_api_path REST API root. Default is `wpcom`.
355 405 *
356 - * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
406 + * @return array Validated arguments.
357 407 */
358 408 public static function validate_args_for_wpcom_json_api_request(
359 409 $path,
360 410 $version = '2',
@@ -369,8 +419,9 @@
369 419 $args,
370 420 array(
371 421 'headers' => 'array',
372 422 'method' => 'string',
423 + 'format' => 'string',
373 424 'timeout' => 'int',
374 425 'redirection' => 'int',
375 426 'stream' => 'boolean',
376 427 'filename' => 'string',
@@ -402,15 +453,16 @@
402 453
403 454 /**
404 455 * Queries the WordPress.com REST API with a user token.
405 456 *
406 - * @param string $path REST API path.
407 - * @param string $version REST API version. Default is `2`.
408 - * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
409 - * @param string $body Body passed to {@see WP_Http}. Default is `null`.
410 - * @param string $base_api_path REST API root. Default is `wpcom`.
457 + * @param string $path REST API path.
458 + * @param string $version REST API version. Default is `2`.
459 + * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
460 + * @param null|string|array $body Body passed to {@see WP_Http}. Default is `null`.
461 + * @param string $base_api_path REST API root. Default is `wpcom`.
411 462 *
412 463 * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
464 + * @phan-return _WP_Remote_Response_Array|WP_Error
413 465 */
414 466 public static function wpcom_json_api_request_as_user(
415 467 $path,
416 468 $version = '2',
@@ -425,9 +477,9 @@
425 477 $args['headers'] = array( 'Content-Type' => 'application/json' );
426 478 }
427 479
428 480 if ( isset( $body ) && ! is_string( $body ) ) {
429 - $body = wp_json_encode( $body );
481 + $body = wp_json_encode( $body, JSON_UNESCAPED_SLASHES );
430 482 }
431 483
432 484 return self::remote_request( $args, $body );
433 485 }
@@ -434,14 +486,15 @@
434 486
435 487 /**
436 488 * Query the WordPress.com REST API using the blog token
437 489 *
438 - * @param String $path The API endpoint relative path.
439 - * @param String $version The API version.
440 - * @param array $args Request arguments.
441 - * @param String $body Request body.
442 - * @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
490 + * @param string $path The API endpoint relative path.
491 + * @param string $version The API version.
492 + * @param array $args Request arguments.
493 + * @param array|string|null $body Request body.
494 + * @param string $base_api_path (optional) the API base path override, defaults to 'rest'.
443 495 * @return array|WP_Error $response Data.
496 + * @phan-return _WP_Remote_Response_Array|WP_Error
444 497 */
445 498 public static function wpcom_json_api_request_as_blog(
446 499 $path,
447 500 $version = self::WPCOM_JSON_API_VERSION,
@@ -466,9 +519,9 @@
466 519 * Takes an array or similar structure and recursively turns all values into strings. This is used to
467 520 * make sure that body hashes are made ith the string version, which is what will be seen after a
468 521 * server pulls up the data in the $_POST array.
469 522 *
470 - * @param array|Mixed $data the data that needs to be stringified.
523 + * @param mixed $data the data that needs to be stringified.
471 524 *
472 525 * @return array|string
473 526 */
474 527 public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore