diff --git a/src/wp-includes/class-wp-http-streams.php b/src/wp-includes/class-wp-http-streams.php index 4ab23b813ba04..b39bc780d5616 100644 --- a/src/wp-includes/class-wp-http-streams.php +++ b/src/wp-includes/class-wp-http-streams.php @@ -20,7 +20,7 @@ class WP_Http_Streams { /** * Send a HTTP request to a URI using PHP Streams. * - * @see WP_Http::request() For default options descriptions. + * @see WP_Http::format_request() For default options descriptions. * * @since 2.7.0 * @since 3.7.0 Combined with the fsockopen transport and switched to stream_socket_client(). diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php index 2e3a9dec22639..3d7b510cb949f 100644 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -108,7 +108,7 @@ class WP_Http { * * @since 2.7.0 * - * @param string $url The request URL. + * @param string|array $url The request URL or an array of requests. * @param string|array $args { * Optional. Array or string of HTTP request arguments. * @@ -167,6 +167,105 @@ class WP_Http { * } */ public function request( $url, $args = array() ) { + if ( is_array( $url ) ) { + $pending_requests = array(); + $responses = array(); + + foreach ( $url as $index => $request ) { + // Support an array of string URLs. + if ( ! is_array( $request ) ) { + $request = array( $request, array() ); + } + + $request = $this->format_request( $request[0], isset( $request[1] ) ? $request[1] : array() ); + + // Handle an error in the pre-request step. Also allow the request to be + // circumvented if the response was short-circuited. + if ( is_wp_error( $request ) ) { + $responses[ $index ] = $request; + continue; + } elseif ( isset( $request['response'] ) ) { + $responses[ $index ] = $request['response']; + continue; + } + + $pending_requests[ $index ] = $request; + } + + if ( ! empty( $pending_requests ) ) { + // Avoid issues where mbstring.func_overload is enabled. + mbstring_binary_safe_encoding(); + + try { + $raw_responses = WpOrg\Requests\Requests::request_multiple( $pending_requests ); + } catch ( WpOrg\Requests\Exception $e ) { + $raw_responses = new WP_Error( 'http_request_failed', $e->getMessage() ); + } + + reset_mbstring_encoding(); + + if ( is_wp_error( $raw_responses ) ) { + return $raw_responses; + } + + foreach ( $pending_requests as $index => $request ) { + $responses[ $index ] = $this->format_response( + $raw_responses[ $index ], + $request['args'], + $request['url'] + ); + } + } + + return $responses; + } + + $formatted = $this->format_request( $url, $args ); + + // Handle an error in the pre-request step. Also allow the request to be + // circumvented if the response was short-circuited. + if ( is_wp_error( $formatted ) ) { + return $formatted; + } elseif ( isset( $formatted['response'] ) ) { + return $formatted['response']; + } + + // Avoid issues where mbstring.func_overload is enabled. + mbstring_binary_safe_encoding(); + + try { + $response = WpOrg\Requests\Requests::request( + $formatted['url'], + $formatted['headers'], + $formatted['data'], + $formatted['type'], + $formatted['options'] + ); + } catch ( WpOrg\Requests\Exception $e ) { + $response = new WP_Error( 'http_request_failed', $e->getMessage() ); + } + + reset_mbstring_encoding(); + + return $this->format_response( $response, $formatted['args'], $formatted['url'] ); + } + + /** + * Format a request to be passed to Requests. + * + * @param string $url URL to request. + * @param array $args Arguments to send to request. + * @return array { + * Array of formatted arguments for the request. + * + * @type string $url URL to request. + * @type array $headers Array of headers for the request. + * @type string $data Data to send with the request. + * @type string $type The type of request to make. + * @type array $options Array of options for the request. + * } + */ + protected function format_request( $url, $args ) { $defaults = array( 'method' => 'GET', /** @@ -241,6 +340,7 @@ public function request( $url, $args = array() ) { } $parsed_args = wp_parse_args( $args, $defaults ); + /** * Filters the arguments used in an HTTP request. * @@ -277,7 +377,9 @@ public function request( $url, $args = array() ) { $pre = apply_filters( 'pre_http_request', false, $parsed_args, $url ); if ( false !== $pre ) { - return $pre; + return array( + 'response' => $pre, + ); } if ( function_exists( 'wp_kses_bad_protocol' ) ) { @@ -408,24 +510,38 @@ public function request( $url, $args = array() ) { } } - // Avoid issues where mbstring.func_overload is enabled. - mbstring_binary_safe_encoding(); + return array( + 'args' => $parsed_args, + 'data' => $data, + 'headers' => $headers, + 'options' => $options, + 'type' => $type, + 'url' => $url, + ); + } - try { - $requests_response = WpOrg\Requests\Requests::request( $url, $headers, $data, $type, $options ); + /** + * Format a response into the expected shape. + * + * @param \WpOrg\Requests\Response|\WpOrg\Requests\Exception|WP_Error $response Response to format. + * @param array $args Request arguments. + * @param string $url Request URL. + * @return array|WP_Error + */ + protected function format_response( $response, $args, $url ) { + if ( $response instanceof \WpOrg\Requests\Exception ) { + $response = new \WP_Error( 'http_request_failed', $response->getMessage() ); + } - // Convert the response into an array. - $http_response = new WP_HTTP_Requests_Response( $requests_response, $parsed_args['filename'] ); + // Convert the response into an array. + if ( ! is_wp_error( $response ) ) { + $http_response = new WP_HTTP_Requests_Response( $response, $args['filename'] ); $response = $http_response->to_array(); // Add the original object to the array. $response['http_response'] = $http_response; - } catch ( WpOrg\Requests\Exception $e ) { - $response = new WP_Error( 'http_request_failed', $e->getMessage() ); } - reset_mbstring_encoding(); - /** * Fires after an HTTP API response is received and before the response is returned. * @@ -434,15 +550,16 @@ public function request( $url, $args = array() ) { * @param array|WP_Error $response HTTP response or WP_Error object. * @param string $context Context under which the hook is fired. * @param string $class HTTP transport used. - * @param array $parsed_args HTTP request arguments. + * @param array $args HTTP request arguments. * @param string $url The request URL. */ - do_action( 'http_api_debug', $response, 'response', 'WpOrg\Requests\Requests', $parsed_args, $url ); + do_action( 'http_api_debug', $response, 'response', \WpOrg\Requests\Requests::class, $args, $url ); + if ( is_wp_error( $response ) ) { return $response; } - if ( ! $parsed_args['blocking'] ) { + if ( ! $args['blocking'] ) { return array( 'headers' => array(), 'body' => '', @@ -464,7 +581,7 @@ public function request( $url, $args = array() ) { * @param array $parsed_args HTTP request arguments. * @param string $url The request URL. */ - return apply_filters( 'http_response', $response, $parsed_args, $url ); + return apply_filters( 'http_response', $response, $args, $url ); } /** @@ -633,9 +750,7 @@ private function _dispatch_request( $url, $args ) { * A WP_Error instance upon error. See WP_Http::response() for details. */ public function post( $url, $args = array() ) { - $defaults = array( 'method' => 'POST' ); - $parsed_args = wp_parse_args( $args, $defaults ); - return $this->request( $url, $parsed_args ); + return $this->normalize_request_args( $url, $args, 'POST' ); } /** @@ -645,15 +760,13 @@ public function post( $url, $args = array() ) { * * @since 2.7.0 * - * @param string $url The request URL. + * @param string|array $url The request URL or array of remote requests that will be run in parallel. * @param string|array $args Optional. Override the defaults. * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. * A WP_Error instance upon error. See WP_Http::response() for details. */ public function get( $url, $args = array() ) { - $defaults = array( 'method' => 'GET' ); - $parsed_args = wp_parse_args( $args, $defaults ); - return $this->request( $url, $parsed_args ); + return $this->normalize_request_args( $url, $args, 'GET' ); } /** @@ -669,8 +782,52 @@ public function get( $url, $args = array() ) { * A WP_Error instance upon error. See WP_Http::response() for details. */ public function head( $url, $args = array() ) { - $defaults = array( 'method' => 'HEAD' ); + return $this->normalize_request_args( $url, $args, 'HEAD' ); + } + + /** + * Normalize the request arguments for a GET, HEAD, or POST request. + * + * @param string|array $url The request URL or array of remote requests that will be run in parallel. + * @param string|array $args Request arguments, optional. Override the defaults. + * @param string $method Request method to make. + * @return array Array containing 'headers', 'body', 'response', 'cookies', 'filename'. + * A WP_Error instance upon error. + */ + protected function normalize_request_args( $url, $args, $method ) { + $defaults = array( 'method' => $method ); + + // Support an array of parallel requests. + if ( is_array( $url ) ) { + $parsed_requests = array(); + + if ( ! empty( $args ) ) { + _doing_it_wrong( + __FUNCTION__, + __( 'Arguments passed to the second $args parameter are ignored when $url is an array of parallel requests.' ), + '6.0.0' + ); + } + + foreach ( $url as $i => $request ) { + // Support an array of string URLs. + if ( ! is_array( $request ) ) { + $request = array( $request, array() ); + } + + list( $request_url, $request_args ) = $request; + + $parsed_requests[ $i ] = array( + $request_url, + wp_parse_args( $request_args, $defaults ), + ); + } + + return $this->request( $parsed_requests ); + } + $parsed_args = wp_parse_args( $args, $defaults ); + return $this->request( $url, $parsed_args ); } diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index b343bb69f572b..ebf8e90ac98a8 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -155,11 +155,11 @@ function wp_safe_remote_head( $url, $args = array() ) { * * @since 2.7.0 * - * @see WP_Http::request() For information on default arguments. + * @see WP_Http::format_request() For information on default arguments. * - * @param string $url URL to retrieve. - * @param array $args Optional. Request arguments. Default empty array. - * See WP_Http::request() for information on accepted arguments. + * @param string|array $url URL to retrieve or array of remote requests that will be run in parallel. + * @param array $args Optional. Request arguments. Default empty array. + * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response array or a WP_Error on failure. * See WP_Http::request() for information on return value. */ @@ -178,9 +178,9 @@ function wp_remote_request( $url, $args = array() ) { * @see wp_remote_request() For more information on the response array format. * @see WP_Http::request() For default arguments information. * - * @param string $url URL to retrieve. - * @param array $args Optional. Request arguments. Default empty array. - * See WP_Http::request() for information on accepted arguments. + * @param string|array $url URL to retrieve or array of remote requests that will be run in parallel. + * @param array $args Optional. Request arguments. Default empty array. + * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. */ @@ -197,11 +197,11 @@ function wp_remote_get( $url, $args = array() ) { * @since 2.7.0 * * @see wp_remote_request() For more information on the response array format. - * @see WP_Http::request() For default arguments information. + * @see WP_Http::format_request() For default arguments information. * - * @param string $url URL to retrieve. - * @param array $args Optional. Request arguments. Default empty array. - * See WP_Http::request() for information on accepted arguments. + * @param string|array $url URL to retrieve or array of remote requests that will be run in parallel. + * @param array $args Optional. Request arguments. Default empty array. + * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. */ @@ -218,11 +218,11 @@ function wp_remote_post( $url, $args = array() ) { * @since 2.7.0 * * @see wp_remote_request() For more information on the response array format. - * @see WP_Http::request() For default arguments information. + * @see WP_Http::format_request() For default arguments information. * - * @param string $url URL to retrieve. - * @param array $args Optional. Request arguments. Default empty array. - * See WP_Http::request() for information on accepted arguments. + * @param string|array $url URL to retrieve or array of remote requests that will be run in parallel. + * @param array $args Optional. Request arguments. Default empty array. + * See WP_Http::request() for information on accepted arguments. * @return array|WP_Error The response or WP_Error on failure. * See WP_Http::request() for information on return value. */ diff --git a/tests/phpunit/tests/http/base.php b/tests/phpunit/tests/http/base.php index 49e4672ad2941..c660c2a48bcbd 100644 --- a/tests/phpunit/tests/http/base.php +++ b/tests/phpunit/tests/http/base.php @@ -466,4 +466,79 @@ public function test_url_with_double_slashes_path() { $this->assertNotWPError( $res ); } + + /** + * Test parallel requests. + * + * @ticket 33055 + * @covers ::wp_remote_request + */ + public function test_parallel_request() { + $responses = wp_remote_request( + array( + 'https://wordpress.org/', + array( + $this->redirection_script . '?code=301&rt=' . 5, + array( + 'method' => 'POST', + 'redirection' => 0, + ), + ), + ) + ); + + list( $request_wp, $request_redirect ) = $responses; + + $this->skipTestOnTimeout( $request_wp ); + $this->skipTestOnTimeout( $request_redirect ); + + $this->assertNotWPError( $request_wp ); + $this->assertNotWPError( $request_redirect ); + + $this->assertSame( 200, wp_remote_retrieve_response_code( $request_wp ) ); + $this->assertSame( 301, wp_remote_retrieve_response_code( $request_redirect ) ); + } + + /** + * Test parallel requests with short circuiting. + * + * @ticket 33055 + * @covers ::wp_remote_request + */ + public function test_parallel_request_short_circuit() { + add_filter( + 'pre_http_request', + function ( $pre, $args, $url ) { + if ( 'https://login.wordpress.org/wp-login.php' === $url ) { + return array( + 'response' => array( + 'code' => 418, + ), + ); + } + + return $pre; + }, + 10, + 3 + ); + + $responses = wp_remote_request( + array( + 'https://wordpress.org/', + 'https://login.wordpress.org/wp-login.php', + ) + ); + + list( $request_wp, $request_login ) = $responses; + + $this->skipTestOnTimeout( $request_wp ); + $this->skipTestOnTimeout( $request_login ); + + $this->assertNotWPError( $request_wp ); + $this->assertNotWPError( $request_login ); + + $this->assertSame( 200, wp_remote_retrieve_response_code( $request_wp ) ); + $this->assertSame( 418, wp_remote_retrieve_response_code( $request_login ) ); + } }