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 +82 -33 13.3.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 */
@@ -104,11 +135,22 @@
104 135 if ( 'header' !== $args['auth_location'] ) {
105 136 $args['auth_location'] = 'query_string';
106 137 }
107 138
108 - $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 + }
109 149 if ( ! $token ) {
110 - 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' );
111 153 }
112 154
113 155 $method = strtoupper( $args['method'] );
114 156
@@ -121,10 +163,10 @@
121 163
122 164 $request = compact( 'method', 'body', 'timeout', 'redirection', 'stream', 'filename', 'sslverify' );
123 165
124 166 @list( $token_key, $secret ) = explode( '.', $token->secret ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
125 - if ( empty( $token ) || empty( $secret ) ) {
126 - return new \WP_Error( 'malformed_token' );
167 + if ( ! $secret ) {
168 + return new WP_Error( 'malformed_token' );
127 169 }
128 170
129 171 $token_key = sprintf(
130 172 '%s:%d:%d',
@@ -140,9 +182,9 @@
140 182
141 183 if ( function_exists( 'wp_generate_password' ) ) {
142 184 $nonce = wp_generate_password( 10, false );
143 185 } else {
144 - $nonce = substr( sha1( wp_rand( 0, 1000000 ) ), 0, 10 );
186 + $nonce = substr( sha1( (string) wp_rand( 0, 1000000 ) ), 0, 10 );
145 187 }
146 188
147 189 // Kind of annoying. Maybe refactor Jetpack_Signature to handle body-hashing.
148 190 if ( $body === null ) {
@@ -158,9 +200,12 @@
158 200 if ( is_array( $body_to_hash ) ) {
159 201 // We cast this to a new variable, because the array form of $body needs to be
160 202 // maintained so it can be passed into the request later on in the code.
161 203 if ( array() !== $body_to_hash ) {
162 - $body_to_hash = wp_json_encode( self::_stringify_data( $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 + );
163 208 } else {
164 209 $body_to_hash = '';
165 210 }
166 211 }
@@ -165,9 +210,9 @@
165 210 }
166 211 }
167 212
168 213 if ( ! is_string( $body_to_hash ) ) {
169 - return new \WP_Error( 'invalid_body', 'Body is malformed.' );
214 + return new WP_Error( 'invalid_body', 'Body is malformed.' );
170 215 }
171 216 $body_hash = base64_encode( sha1( $body_to_hash, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
172 217 }
173 218
@@ -194,9 +239,9 @@
194 239 $url = add_query_arg( urlencode_deep( $url_args ), $args['url'] );
195 240
196 241 $signature = $jetpack_signature->sign_request( $token_key, $timestamp, $nonce, $body_hash, $method, $url, $body, false );
197 242
198 - if ( ! $signature || is_wp_error( $signature ) ) {
243 + if ( is_wp_error( $signature ) ) {
199 244 return $signature;
200 245 }
201 246
202 247 // Send an Authorization header so various caches/proxies do the right thing.
@@ -231,12 +276,13 @@
231 276 * The option is checked on each request.
232 277 *
233 278 * @internal
234 279 *
235 - * @param String $url the request URL.
280 + * @param string $url the request URL.
236 281 * @param array $args request arguments.
237 - * @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.
238 283 * @return array|WP_Error WP HTTP response on success
284 + * @phan-return _WP_Remote_Response_Array|WP_Error
239 285 */
240 286 public static function _wp_remote_request( $url, $args, $set_fallback = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
241 287 $fallback = \Jetpack_Options::get_option( 'fallback_no_verify_ssl_certs' );
242 288 if ( false === $fallback ) {
@@ -314,10 +360,11 @@
314 360
315 361 /**
316 362 * Sets the time difference for correct signature computation.
317 363 *
318 - * @param HTTP_Response $response the response object.
319 - * @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
320 367 */
321 368 public static function set_time_diff( &$response, $force_set = false ) {
322 369 $code = wp_remote_retrieve_response_code( $response );
323 370
@@ -355,9 +402,9 @@
355 402 * @param string $version REST API version. Default is `2`.
356 403 * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
357 404 * @param string $base_api_path REST API root. Default is `wpcom`.
358 405 *
359 - * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
406 + * @return array Validated arguments.
360 407 */
361 408 public static function validate_args_for_wpcom_json_api_request(
362 409 $path,
363 410 $version = '2',
@@ -406,15 +453,16 @@
406 453
407 454 /**
408 455 * Queries the WordPress.com REST API with a user token.
409 456 *
410 - * @param string $path REST API path.
411 - * @param string $version REST API version. Default is `2`.
412 - * @param array $args Arguments to {@see WP_Http}. Default is `array()`.
413 - * @param string $body Body passed to {@see WP_Http}. Default is `null`.
414 - * @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`.
415 462 *
416 463 * @return array|WP_Error $response Response data, else {@see WP_Error} on failure.
464 + * @phan-return _WP_Remote_Response_Array|WP_Error
417 465 */
418 466 public static function wpcom_json_api_request_as_user(
419 467 $path,
420 468 $version = '2',
@@ -429,9 +477,9 @@
429 477 $args['headers'] = array( 'Content-Type' => 'application/json' );
430 478 }
431 479
432 480 if ( isset( $body ) && ! is_string( $body ) ) {
433 - $body = wp_json_encode( $body );
481 + $body = wp_json_encode( $body, JSON_UNESCAPED_SLASHES );
434 482 }
435 483
436 484 return self::remote_request( $args, $body );
437 485 }
@@ -438,14 +486,15 @@
438 486
439 487 /**
440 488 * Query the WordPress.com REST API using the blog token
441 489 *
442 - * @param String $path The API endpoint relative path.
443 - * @param String $version The API version.
444 - * @param array $args Request arguments.
445 - * @param String $body Request body.
446 - * @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'.
447 495 * @return array|WP_Error $response Data.
496 + * @phan-return _WP_Remote_Response_Array|WP_Error
448 497 */
449 498 public static function wpcom_json_api_request_as_blog(
450 499 $path,
451 500 $version = self::WPCOM_JSON_API_VERSION,
@@ -470,9 +519,9 @@
470 519 * Takes an array or similar structure and recursively turns all values into strings. This is used to
471 520 * make sure that body hashes are made ith the string version, which is what will be seen after a
472 521 * server pulls up the data in the $_POST array.
473 522 *
474 - * @param array|Mixed $data the data that needs to be stringified.
523 + * @param mixed $data the data that needs to be stringified.
475 524 *
476 525 * @return array|string
477 526 */
478 527 public static function _stringify_data( $data ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore