← All changes
|
jetpack_vendor/automattic/jetpack-connection/src/class-client.php
+89
-36
12.7.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 ( array() !== $body ) { | |
| 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. |
| @@ -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 |