wp/wp-includes/Requests/src/Requests.php
changeset 21 48c4eec2b7e6
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * Requests for PHP
       
     4  *
       
     5  * Inspired by Requests for Python.
       
     6  *
       
     7  * Based on concepts from SimplePie_File, RequestCore and WP_Http.
       
     8  *
       
     9  * @package Requests
       
    10  */
       
    11 
       
    12 namespace WpOrg\Requests;
       
    13 
       
    14 use WpOrg\Requests\Auth\Basic;
       
    15 use WpOrg\Requests\Capability;
       
    16 use WpOrg\Requests\Cookie\Jar;
       
    17 use WpOrg\Requests\Exception;
       
    18 use WpOrg\Requests\Exception\InvalidArgument;
       
    19 use WpOrg\Requests\Hooks;
       
    20 use WpOrg\Requests\IdnaEncoder;
       
    21 use WpOrg\Requests\Iri;
       
    22 use WpOrg\Requests\Proxy\Http;
       
    23 use WpOrg\Requests\Response;
       
    24 use WpOrg\Requests\Transport\Curl;
       
    25 use WpOrg\Requests\Transport\Fsockopen;
       
    26 use WpOrg\Requests\Utility\InputValidator;
       
    27 
       
    28 /**
       
    29  * Requests for PHP
       
    30  *
       
    31  * Inspired by Requests for Python.
       
    32  *
       
    33  * Based on concepts from SimplePie_File, RequestCore and WP_Http.
       
    34  *
       
    35  * @package Requests
       
    36  */
       
    37 class Requests {
       
    38 	/**
       
    39 	 * POST method
       
    40 	 *
       
    41 	 * @var string
       
    42 	 */
       
    43 	const POST = 'POST';
       
    44 
       
    45 	/**
       
    46 	 * PUT method
       
    47 	 *
       
    48 	 * @var string
       
    49 	 */
       
    50 	const PUT = 'PUT';
       
    51 
       
    52 	/**
       
    53 	 * GET method
       
    54 	 *
       
    55 	 * @var string
       
    56 	 */
       
    57 	const GET = 'GET';
       
    58 
       
    59 	/**
       
    60 	 * HEAD method
       
    61 	 *
       
    62 	 * @var string
       
    63 	 */
       
    64 	const HEAD = 'HEAD';
       
    65 
       
    66 	/**
       
    67 	 * DELETE method
       
    68 	 *
       
    69 	 * @var string
       
    70 	 */
       
    71 	const DELETE = 'DELETE';
       
    72 
       
    73 	/**
       
    74 	 * OPTIONS method
       
    75 	 *
       
    76 	 * @var string
       
    77 	 */
       
    78 	const OPTIONS = 'OPTIONS';
       
    79 
       
    80 	/**
       
    81 	 * TRACE method
       
    82 	 *
       
    83 	 * @var string
       
    84 	 */
       
    85 	const TRACE = 'TRACE';
       
    86 
       
    87 	/**
       
    88 	 * PATCH method
       
    89 	 *
       
    90 	 * @link https://tools.ietf.org/html/rfc5789
       
    91 	 * @var string
       
    92 	 */
       
    93 	const PATCH = 'PATCH';
       
    94 
       
    95 	/**
       
    96 	 * Default size of buffer size to read streams
       
    97 	 *
       
    98 	 * @var integer
       
    99 	 */
       
   100 	const BUFFER_SIZE = 1160;
       
   101 
       
   102 	/**
       
   103 	 * Option defaults.
       
   104 	 *
       
   105 	 * @see \WpOrg\Requests\Requests::get_default_options()
       
   106 	 * @see \WpOrg\Requests\Requests::request() for values returned by this method
       
   107 	 *
       
   108 	 * @since 2.0.0
       
   109 	 *
       
   110 	 * @var array
       
   111 	 */
       
   112 	const OPTION_DEFAULTS = [
       
   113 		'timeout'          => 10,
       
   114 		'connect_timeout'  => 10,
       
   115 		'useragent'        => 'php-requests/' . self::VERSION,
       
   116 		'protocol_version' => 1.1,
       
   117 		'redirected'       => 0,
       
   118 		'redirects'        => 10,
       
   119 		'follow_redirects' => true,
       
   120 		'blocking'         => true,
       
   121 		'type'             => self::GET,
       
   122 		'filename'         => false,
       
   123 		'auth'             => false,
       
   124 		'proxy'            => false,
       
   125 		'cookies'          => false,
       
   126 		'max_bytes'        => false,
       
   127 		'idn'              => true,
       
   128 		'hooks'            => null,
       
   129 		'transport'        => null,
       
   130 		'verify'           => null,
       
   131 		'verifyname'       => true,
       
   132 	];
       
   133 
       
   134 	/**
       
   135 	 * Default supported Transport classes.
       
   136 	 *
       
   137 	 * @since 2.0.0
       
   138 	 *
       
   139 	 * @var array
       
   140 	 */
       
   141 	const DEFAULT_TRANSPORTS = [
       
   142 		Curl::class      => Curl::class,
       
   143 		Fsockopen::class => Fsockopen::class,
       
   144 	];
       
   145 
       
   146 	/**
       
   147 	 * Current version of Requests
       
   148 	 *
       
   149 	 * @var string
       
   150 	 */
       
   151 	const VERSION = '2.0.11';
       
   152 
       
   153 	/**
       
   154 	 * Selected transport name
       
   155 	 *
       
   156 	 * Use {@see \WpOrg\Requests\Requests::get_transport()} instead
       
   157 	 *
       
   158 	 * @var array
       
   159 	 */
       
   160 	public static $transport = [];
       
   161 
       
   162 	/**
       
   163 	 * Registered transport classes
       
   164 	 *
       
   165 	 * @var array
       
   166 	 */
       
   167 	protected static $transports = [];
       
   168 
       
   169 	/**
       
   170 	 * Default certificate path.
       
   171 	 *
       
   172 	 * @see \WpOrg\Requests\Requests::get_certificate_path()
       
   173 	 * @see \WpOrg\Requests\Requests::set_certificate_path()
       
   174 	 *
       
   175 	 * @var string
       
   176 	 */
       
   177 	protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem';
       
   178 
       
   179 	/**
       
   180 	 * All (known) valid deflate, gzip header magic markers.
       
   181 	 *
       
   182 	 * These markers relate to different compression levels.
       
   183 	 *
       
   184 	 * @link https://stackoverflow.com/a/43170354/482864 Marker source.
       
   185 	 *
       
   186 	 * @since 2.0.0
       
   187 	 *
       
   188 	 * @var array
       
   189 	 */
       
   190 	private static $magic_compression_headers = [
       
   191 		"\x1f\x8b" => true, // Gzip marker.
       
   192 		"\x78\x01" => true, // Zlib marker - level 1.
       
   193 		"\x78\x5e" => true, // Zlib marker - level 2 to 5.
       
   194 		"\x78\x9c" => true, // Zlib marker - level 6.
       
   195 		"\x78\xda" => true, // Zlib marker - level 7 to 9.
       
   196 	];
       
   197 
       
   198 	/**
       
   199 	 * This is a static class, do not instantiate it
       
   200 	 *
       
   201 	 * @codeCoverageIgnore
       
   202 	 */
       
   203 	private function __construct() {}
       
   204 
       
   205 	/**
       
   206 	 * Register a transport
       
   207 	 *
       
   208 	 * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface
       
   209 	 */
       
   210 	public static function add_transport($transport) {
       
   211 		if (empty(self::$transports)) {
       
   212 			self::$transports = self::DEFAULT_TRANSPORTS;
       
   213 		}
       
   214 
       
   215 		self::$transports[$transport] = $transport;
       
   216 	}
       
   217 
       
   218 	/**
       
   219 	 * Get the fully qualified class name (FQCN) for a working transport.
       
   220 	 *
       
   221 	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
       
   222 	 * @return string FQCN of the transport to use, or an empty string if no transport was
       
   223 	 *                found which provided the requested capabilities.
       
   224 	 */
       
   225 	protected static function get_transport_class(array $capabilities = []) {
       
   226 		// Caching code, don't bother testing coverage.
       
   227 		// @codeCoverageIgnoreStart
       
   228 		// Array of capabilities as a string to be used as an array key.
       
   229 		ksort($capabilities);
       
   230 		$cap_string = serialize($capabilities);
       
   231 
       
   232 		// Don't search for a transport if it's already been done for these $capabilities.
       
   233 		if (isset(self::$transport[$cap_string])) {
       
   234 			return self::$transport[$cap_string];
       
   235 		}
       
   236 
       
   237 		// Ensure we will not run this same check again later on.
       
   238 		self::$transport[$cap_string] = '';
       
   239 		// @codeCoverageIgnoreEnd
       
   240 
       
   241 		if (empty(self::$transports)) {
       
   242 			self::$transports = self::DEFAULT_TRANSPORTS;
       
   243 		}
       
   244 
       
   245 		// Find us a working transport.
       
   246 		foreach (self::$transports as $class) {
       
   247 			if (!class_exists($class)) {
       
   248 				continue;
       
   249 			}
       
   250 
       
   251 			$result = $class::test($capabilities);
       
   252 			if ($result === true) {
       
   253 				self::$transport[$cap_string] = $class;
       
   254 				break;
       
   255 			}
       
   256 		}
       
   257 
       
   258 		return self::$transport[$cap_string];
       
   259 	}
       
   260 
       
   261 	/**
       
   262 	 * Get a working transport.
       
   263 	 *
       
   264 	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
       
   265 	 * @return \WpOrg\Requests\Transport
       
   266 	 * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`).
       
   267 	 */
       
   268 	protected static function get_transport(array $capabilities = []) {
       
   269 		$class = self::get_transport_class($capabilities);
       
   270 
       
   271 		if ($class === '') {
       
   272 			throw new Exception('No working transports found', 'notransport', self::$transports);
       
   273 		}
       
   274 
       
   275 		return new $class();
       
   276 	}
       
   277 
       
   278 	/**
       
   279 	 * Checks to see if we have a transport for the capabilities requested.
       
   280 	 *
       
   281 	 * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability}
       
   282 	 * interface as constants.
       
   283 	 *
       
   284 	 * Example usage:
       
   285 	 * `Requests::has_capabilities([Capability::SSL => true])`.
       
   286 	 *
       
   287 	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
       
   288 	 * @return bool Whether the transport has the requested capabilities.
       
   289 	 */
       
   290 	public static function has_capabilities(array $capabilities = []) {
       
   291 		return self::get_transport_class($capabilities) !== '';
       
   292 	}
       
   293 
       
   294 	/**#@+
       
   295 	 * @see \WpOrg\Requests\Requests::request()
       
   296 	 * @param string $url
       
   297 	 * @param array $headers
       
   298 	 * @param array $options
       
   299 	 * @return \WpOrg\Requests\Response
       
   300 	 */
       
   301 	/**
       
   302 	 * Send a GET request
       
   303 	 */
       
   304 	public static function get($url, $headers = [], $options = []) {
       
   305 		return self::request($url, $headers, null, self::GET, $options);
       
   306 	}
       
   307 
       
   308 	/**
       
   309 	 * Send a HEAD request
       
   310 	 */
       
   311 	public static function head($url, $headers = [], $options = []) {
       
   312 		return self::request($url, $headers, null, self::HEAD, $options);
       
   313 	}
       
   314 
       
   315 	/**
       
   316 	 * Send a DELETE request
       
   317 	 */
       
   318 	public static function delete($url, $headers = [], $options = []) {
       
   319 		return self::request($url, $headers, null, self::DELETE, $options);
       
   320 	}
       
   321 
       
   322 	/**
       
   323 	 * Send a TRACE request
       
   324 	 */
       
   325 	public static function trace($url, $headers = [], $options = []) {
       
   326 		return self::request($url, $headers, null, self::TRACE, $options);
       
   327 	}
       
   328 	/**#@-*/
       
   329 
       
   330 	/**#@+
       
   331 	 * @see \WpOrg\Requests\Requests::request()
       
   332 	 * @param string $url
       
   333 	 * @param array $headers
       
   334 	 * @param array $data
       
   335 	 * @param array $options
       
   336 	 * @return \WpOrg\Requests\Response
       
   337 	 */
       
   338 	/**
       
   339 	 * Send a POST request
       
   340 	 */
       
   341 	public static function post($url, $headers = [], $data = [], $options = []) {
       
   342 		return self::request($url, $headers, $data, self::POST, $options);
       
   343 	}
       
   344 	/**
       
   345 	 * Send a PUT request
       
   346 	 */
       
   347 	public static function put($url, $headers = [], $data = [], $options = []) {
       
   348 		return self::request($url, $headers, $data, self::PUT, $options);
       
   349 	}
       
   350 
       
   351 	/**
       
   352 	 * Send an OPTIONS request
       
   353 	 */
       
   354 	public static function options($url, $headers = [], $data = [], $options = []) {
       
   355 		return self::request($url, $headers, $data, self::OPTIONS, $options);
       
   356 	}
       
   357 
       
   358 	/**
       
   359 	 * Send a PATCH request
       
   360 	 *
       
   361 	 * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()},
       
   362 	 * `$headers` is required, as the specification recommends that should send an ETag
       
   363 	 *
       
   364 	 * @link https://tools.ietf.org/html/rfc5789
       
   365 	 */
       
   366 	public static function patch($url, $headers, $data = [], $options = []) {
       
   367 		return self::request($url, $headers, $data, self::PATCH, $options);
       
   368 	}
       
   369 	/**#@-*/
       
   370 
       
   371 	/**
       
   372 	 * Main interface for HTTP requests
       
   373 	 *
       
   374 	 * This method initiates a request and sends it via a transport before
       
   375 	 * parsing.
       
   376 	 *
       
   377 	 * The `$options` parameter takes an associative array with the following
       
   378 	 * options:
       
   379 	 *
       
   380 	 * - `timeout`: How long should we wait for a response?
       
   381 	 *    Note: for cURL, a minimum of 1 second applies, as DNS resolution
       
   382 	 *    operates at second-resolution only.
       
   383 	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
       
   384 	 * - `connect_timeout`: How long should we wait while trying to connect?
       
   385 	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
       
   386 	 * - `useragent`: Useragent to send to the server
       
   387 	 *    (string, default: php-requests/$version)
       
   388 	 * - `follow_redirects`: Should we follow 3xx redirects?
       
   389 	 *    (boolean, default: true)
       
   390 	 * - `redirects`: How many times should we redirect before erroring?
       
   391 	 *    (integer, default: 10)
       
   392 	 * - `blocking`: Should we block processing on this request?
       
   393 	 *    (boolean, default: true)
       
   394 	 * - `filename`: File to stream the body to instead.
       
   395 	 *    (string|boolean, default: false)
       
   396 	 * - `auth`: Authentication handler or array of user/password details to use
       
   397 	 *    for Basic authentication
       
   398 	 *    (\WpOrg\Requests\Auth|array|boolean, default: false)
       
   399 	 * - `proxy`: Proxy details to use for proxy by-passing and authentication
       
   400 	 *    (\WpOrg\Requests\Proxy|array|string|boolean, default: false)
       
   401 	 * - `max_bytes`: Limit for the response body size.
       
   402 	 *    (integer|boolean, default: false)
       
   403 	 * - `idn`: Enable IDN parsing
       
   404 	 *    (boolean, default: true)
       
   405 	 * - `transport`: Custom transport. Either a class name, or a
       
   406 	 *    transport object. Defaults to the first working transport from
       
   407 	 *    {@see \WpOrg\Requests\Requests::getTransport()}
       
   408 	 *    (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()})
       
   409 	 * - `hooks`: Hooks handler.
       
   410 	 *    (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks())
       
   411 	 * - `verify`: Should we verify SSL certificates? Allows passing in a custom
       
   412 	 *    certificate file as a string. (Using true uses the system-wide root
       
   413 	 *    certificate store instead, but this may have different behaviour
       
   414 	 *    across transports.)
       
   415 	 *    (string|boolean, default: certificates/cacert.pem)
       
   416 	 * - `verifyname`: Should we verify the common name in the SSL certificate?
       
   417 	 *    (boolean, default: true)
       
   418 	 * - `data_format`: How should we send the `$data` parameter?
       
   419 	 *    (string, one of 'query' or 'body', default: 'query' for
       
   420 	 *    HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH)
       
   421 	 *
       
   422 	 * @param string|Stringable $url URL to request
       
   423 	 * @param array $headers Extra headers to send with the request
       
   424 	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
       
   425 	 * @param string $type HTTP request type (use Requests constants)
       
   426 	 * @param array $options Options for the request (see description for more information)
       
   427 	 * @return \WpOrg\Requests\Response
       
   428 	 *
       
   429 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable.
       
   430 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string.
       
   431 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
       
   432 	 * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`)
       
   433 	 */
       
   434 	public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) {
       
   435 		if (InputValidator::is_string_or_stringable($url) === false) {
       
   436 			throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url));
       
   437 		}
       
   438 
       
   439 		if (is_string($type) === false) {
       
   440 			throw InvalidArgument::create(4, '$type', 'string', gettype($type));
       
   441 		}
       
   442 
       
   443 		if (is_array($options) === false) {
       
   444 			throw InvalidArgument::create(5, '$options', 'array', gettype($options));
       
   445 		}
       
   446 
       
   447 		if (empty($options['type'])) {
       
   448 			$options['type'] = $type;
       
   449 		}
       
   450 
       
   451 		$options = array_merge(self::get_default_options(), $options);
       
   452 
       
   453 		self::set_defaults($url, $headers, $data, $type, $options);
       
   454 
       
   455 		$options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]);
       
   456 
       
   457 		if (!empty($options['transport'])) {
       
   458 			$transport = $options['transport'];
       
   459 
       
   460 			if (is_string($options['transport'])) {
       
   461 				$transport = new $transport();
       
   462 			}
       
   463 		} else {
       
   464 			$need_ssl     = (stripos($url, 'https://') === 0);
       
   465 			$capabilities = [Capability::SSL => $need_ssl];
       
   466 			$transport    = self::get_transport($capabilities);
       
   467 		}
       
   468 
       
   469 		$response = $transport->request($url, $headers, $data, $options);
       
   470 
       
   471 		$options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]);
       
   472 
       
   473 		return self::parse_response($response, $url, $headers, $data, $options);
       
   474 	}
       
   475 
       
   476 	/**
       
   477 	 * Send multiple HTTP requests simultaneously
       
   478 	 *
       
   479 	 * The `$requests` parameter takes an associative or indexed array of
       
   480 	 * request fields. The key of each request can be used to match up the
       
   481 	 * request with the returned data, or with the request passed into your
       
   482 	 * `multiple.request.complete` callback.
       
   483 	 *
       
   484 	 * The request fields value is an associative array with the following keys:
       
   485 	 *
       
   486 	 * - `url`: Request URL Same as the `$url` parameter to
       
   487 	 *    {@see \WpOrg\Requests\Requests::request()}
       
   488 	 *    (string, required)
       
   489 	 * - `headers`: Associative array of header fields. Same as the `$headers`
       
   490 	 *    parameter to {@see \WpOrg\Requests\Requests::request()}
       
   491 	 *    (array, default: `array()`)
       
   492 	 * - `data`: Associative array of data fields or a string. Same as the
       
   493 	 *    `$data` parameter to {@see \WpOrg\Requests\Requests::request()}
       
   494 	 *    (array|string, default: `array()`)
       
   495 	 * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type`
       
   496 	 *    parameter to {@see \WpOrg\Requests\Requests::request()}
       
   497 	 *    (string, default: `\WpOrg\Requests\Requests::GET`)
       
   498 	 * - `cookies`: Associative array of cookie name to value, or cookie jar.
       
   499 	 *    (array|\WpOrg\Requests\Cookie\Jar)
       
   500 	 *
       
   501 	 * If the `$options` parameter is specified, individual requests will
       
   502 	 * inherit options from it. This can be used to use a single hooking system,
       
   503 	 * or set all the types to `\WpOrg\Requests\Requests::POST`, for example.
       
   504 	 *
       
   505 	 * In addition, the `$options` parameter takes the following global options:
       
   506 	 *
       
   507 	 * - `complete`: A callback for when a request is complete. Takes two
       
   508 	 *    parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the
       
   509 	 *    ID from the request array (Note: this can also be overridden on a
       
   510 	 *    per-request basis, although that's a little silly)
       
   511 	 *    (callback)
       
   512 	 *
       
   513 	 * @param array $requests Requests data (see description for more information)
       
   514 	 * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()})
       
   515 	 * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object)
       
   516 	 *
       
   517 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
       
   518 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
       
   519 	 */
       
   520 	public static function request_multiple($requests, $options = []) {
       
   521 		if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
       
   522 			throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
       
   523 		}
       
   524 
       
   525 		if (is_array($options) === false) {
       
   526 			throw InvalidArgument::create(2, '$options', 'array', gettype($options));
       
   527 		}
       
   528 
       
   529 		$options = array_merge(self::get_default_options(true), $options);
       
   530 
       
   531 		if (!empty($options['hooks'])) {
       
   532 			$options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
       
   533 			if (!empty($options['complete'])) {
       
   534 				$options['hooks']->register('multiple.request.complete', $options['complete']);
       
   535 			}
       
   536 		}
       
   537 
       
   538 		foreach ($requests as $id => &$request) {
       
   539 			if (!isset($request['headers'])) {
       
   540 				$request['headers'] = [];
       
   541 			}
       
   542 
       
   543 			if (!isset($request['data'])) {
       
   544 				$request['data'] = [];
       
   545 			}
       
   546 
       
   547 			if (!isset($request['type'])) {
       
   548 				$request['type'] = self::GET;
       
   549 			}
       
   550 
       
   551 			if (!isset($request['options'])) {
       
   552 				$request['options']         = $options;
       
   553 				$request['options']['type'] = $request['type'];
       
   554 			} else {
       
   555 				if (empty($request['options']['type'])) {
       
   556 					$request['options']['type'] = $request['type'];
       
   557 				}
       
   558 
       
   559 				$request['options'] = array_merge($options, $request['options']);
       
   560 			}
       
   561 
       
   562 			self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);
       
   563 
       
   564 			// Ensure we only hook in once
       
   565 			if ($request['options']['hooks'] !== $options['hooks']) {
       
   566 				$request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
       
   567 				if (!empty($request['options']['complete'])) {
       
   568 					$request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
       
   569 				}
       
   570 			}
       
   571 		}
       
   572 
       
   573 		unset($request);
       
   574 
       
   575 		if (!empty($options['transport'])) {
       
   576 			$transport = $options['transport'];
       
   577 
       
   578 			if (is_string($options['transport'])) {
       
   579 				$transport = new $transport();
       
   580 			}
       
   581 		} else {
       
   582 			$transport = self::get_transport();
       
   583 		}
       
   584 
       
   585 		$responses = $transport->request_multiple($requests, $options);
       
   586 
       
   587 		foreach ($responses as $id => &$response) {
       
   588 			// If our hook got messed with somehow, ensure we end up with the
       
   589 			// correct response
       
   590 			if (is_string($response)) {
       
   591 				$request = $requests[$id];
       
   592 				self::parse_multiple($response, $request);
       
   593 				$request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]);
       
   594 			}
       
   595 		}
       
   596 
       
   597 		return $responses;
       
   598 	}
       
   599 
       
   600 	/**
       
   601 	 * Get the default options
       
   602 	 *
       
   603 	 * @see \WpOrg\Requests\Requests::request() for values returned by this method
       
   604 	 * @param boolean $multirequest Is this a multirequest?
       
   605 	 * @return array Default option values
       
   606 	 */
       
   607 	protected static function get_default_options($multirequest = false) {
       
   608 		$defaults           = static::OPTION_DEFAULTS;
       
   609 		$defaults['verify'] = self::$certificate_path;
       
   610 
       
   611 		if ($multirequest !== false) {
       
   612 			$defaults['complete'] = null;
       
   613 		}
       
   614 
       
   615 		return $defaults;
       
   616 	}
       
   617 
       
   618 	/**
       
   619 	 * Get default certificate path.
       
   620 	 *
       
   621 	 * @return string Default certificate path.
       
   622 	 */
       
   623 	public static function get_certificate_path() {
       
   624 		return self::$certificate_path;
       
   625 	}
       
   626 
       
   627 	/**
       
   628 	 * Set default certificate path.
       
   629 	 *
       
   630 	 * @param string|Stringable|bool $path Certificate path, pointing to a PEM file.
       
   631 	 *
       
   632 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean.
       
   633 	 */
       
   634 	public static function set_certificate_path($path) {
       
   635 		if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) {
       
   636 			throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path));
       
   637 		}
       
   638 
       
   639 		self::$certificate_path = $path;
       
   640 	}
       
   641 
       
   642 	/**
       
   643 	 * Set the default values
       
   644 	 *
       
   645 	 * The $options parameter is updated with the results.
       
   646 	 *
       
   647 	 * @param string $url URL to request
       
   648 	 * @param array $headers Extra headers to send with the request
       
   649 	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
       
   650 	 * @param string $type HTTP request type
       
   651 	 * @param array $options Options for the request
       
   652 	 * @return void
       
   653 	 *
       
   654 	 * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL.
       
   655 	 */
       
   656 	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
       
   657 		if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
       
   658 			throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
       
   659 		}
       
   660 
       
   661 		if (empty($options['hooks'])) {
       
   662 			$options['hooks'] = new Hooks();
       
   663 		}
       
   664 
       
   665 		if (is_array($options['auth'])) {
       
   666 			$options['auth'] = new Basic($options['auth']);
       
   667 		}
       
   668 
       
   669 		if ($options['auth'] !== false) {
       
   670 			$options['auth']->register($options['hooks']);
       
   671 		}
       
   672 
       
   673 		if (is_string($options['proxy']) || is_array($options['proxy'])) {
       
   674 			$options['proxy'] = new Http($options['proxy']);
       
   675 		}
       
   676 
       
   677 		if ($options['proxy'] !== false) {
       
   678 			$options['proxy']->register($options['hooks']);
       
   679 		}
       
   680 
       
   681 		if (is_array($options['cookies'])) {
       
   682 			$options['cookies'] = new Jar($options['cookies']);
       
   683 		} elseif (empty($options['cookies'])) {
       
   684 			$options['cookies'] = new Jar();
       
   685 		}
       
   686 
       
   687 		if ($options['cookies'] !== false) {
       
   688 			$options['cookies']->register($options['hooks']);
       
   689 		}
       
   690 
       
   691 		if ($options['idn'] !== false) {
       
   692 			$iri       = new Iri($url);
       
   693 			$iri->host = IdnaEncoder::encode($iri->ihost);
       
   694 			$url       = $iri->uri;
       
   695 		}
       
   696 
       
   697 		// Massage the type to ensure we support it.
       
   698 		$type = strtoupper($type);
       
   699 
       
   700 		if (!isset($options['data_format'])) {
       
   701 			if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) {
       
   702 				$options['data_format'] = 'query';
       
   703 			} else {
       
   704 				$options['data_format'] = 'body';
       
   705 			}
       
   706 		}
       
   707 	}
       
   708 
       
   709 	/**
       
   710 	 * HTTP response parser
       
   711 	 *
       
   712 	 * @param string $headers Full response text including headers and body
       
   713 	 * @param string $url Original request URL
       
   714 	 * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
       
   715 	 * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
       
   716 	 * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
       
   717 	 * @return \WpOrg\Requests\Response
       
   718 	 *
       
   719 	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`)
       
   720 	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`)
       
   721 	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`)
       
   722 	 */
       
   723 	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
       
   724 		$return = new Response();
       
   725 		if (!$options['blocking']) {
       
   726 			return $return;
       
   727 		}
       
   728 
       
   729 		$return->raw  = $headers;
       
   730 		$return->url  = (string) $url;
       
   731 		$return->body = '';
       
   732 
       
   733 		if (!$options['filename']) {
       
   734 			$pos = strpos($headers, "\r\n\r\n");
       
   735 			if ($pos === false) {
       
   736 				// Crap!
       
   737 				throw new Exception('Missing header/body separator', 'requests.no_crlf_separator');
       
   738 			}
       
   739 
       
   740 			$headers = substr($return->raw, 0, $pos);
       
   741 			// Headers will always be separated from the body by two new lines - `\n\r\n\r`.
       
   742 			$body = substr($return->raw, $pos + 4);
       
   743 			if (!empty($body)) {
       
   744 				$return->body = $body;
       
   745 			}
       
   746 		}
       
   747 
       
   748 		// Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
       
   749 		$headers = str_replace("\r\n", "\n", $headers);
       
   750 		// Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
       
   751 		$headers = preg_replace('/\n[ \t]/', ' ', $headers);
       
   752 		$headers = explode("\n", $headers);
       
   753 		preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
       
   754 		if (empty($matches)) {
       
   755 			throw new Exception('Response could not be parsed', 'noversion', $headers);
       
   756 		}
       
   757 
       
   758 		$return->protocol_version = (float) $matches[1];
       
   759 		$return->status_code      = (int) $matches[2];
       
   760 		if ($return->status_code >= 200 && $return->status_code < 300) {
       
   761 			$return->success = true;
       
   762 		}
       
   763 
       
   764 		foreach ($headers as $header) {
       
   765 			list($key, $value) = explode(':', $header, 2);
       
   766 			$value             = trim($value);
       
   767 			preg_replace('#(\s+)#i', ' ', $value);
       
   768 			$return->headers[$key] = $value;
       
   769 		}
       
   770 
       
   771 		if (isset($return->headers['transfer-encoding'])) {
       
   772 			$return->body = self::decode_chunked($return->body);
       
   773 			unset($return->headers['transfer-encoding']);
       
   774 		}
       
   775 
       
   776 		if (isset($return->headers['content-encoding'])) {
       
   777 			$return->body = self::decompress($return->body);
       
   778 		}
       
   779 
       
   780 		//fsockopen and cURL compatibility
       
   781 		if (isset($return->headers['connection'])) {
       
   782 			unset($return->headers['connection']);
       
   783 		}
       
   784 
       
   785 		$options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]);
       
   786 
       
   787 		if ($return->is_redirect() && $options['follow_redirects'] === true) {
       
   788 			if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
       
   789 				if ($return->status_code === 303) {
       
   790 					$options['type'] = self::GET;
       
   791 				}
       
   792 
       
   793 				$options['redirected']++;
       
   794 				$location = $return->headers['location'];
       
   795 				if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
       
   796 					// relative redirect, for compatibility make it absolute
       
   797 					$location = Iri::absolutize($url, $location);
       
   798 					$location = $location->uri;
       
   799 				}
       
   800 
       
   801 				$hook_args = [
       
   802 					&$location,
       
   803 					&$req_headers,
       
   804 					&$req_data,
       
   805 					&$options,
       
   806 					$return,
       
   807 				];
       
   808 				$options['hooks']->dispatch('requests.before_redirect', $hook_args);
       
   809 				$redirected            = self::request($location, $req_headers, $req_data, $options['type'], $options);
       
   810 				$redirected->history[] = $return;
       
   811 				return $redirected;
       
   812 			} elseif ($options['redirected'] >= $options['redirects']) {
       
   813 				throw new Exception('Too many redirects', 'toomanyredirects', $return);
       
   814 			}
       
   815 		}
       
   816 
       
   817 		$return->redirects = $options['redirected'];
       
   818 
       
   819 		$options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]);
       
   820 		return $return;
       
   821 	}
       
   822 
       
   823 	/**
       
   824 	 * Callback for `transport.internal.parse_response`
       
   825 	 *
       
   826 	 * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response
       
   827 	 * while still executing a multiple request.
       
   828 	 *
       
   829 	 * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object
       
   830 	 *
       
   831 	 * @param string $response Full response text including headers and body (will be overwritten with Response instance)
       
   832 	 * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()}
       
   833 	 * @return void
       
   834 	 */
       
   835 	public static function parse_multiple(&$response, $request) {
       
   836 		try {
       
   837 			$url      = $request['url'];
       
   838 			$headers  = $request['headers'];
       
   839 			$data     = $request['data'];
       
   840 			$options  = $request['options'];
       
   841 			$response = self::parse_response($response, $url, $headers, $data, $options);
       
   842 		} catch (Exception $e) {
       
   843 			$response = $e;
       
   844 		}
       
   845 	}
       
   846 
       
   847 	/**
       
   848 	 * Decoded a chunked body as per RFC 2616
       
   849 	 *
       
   850 	 * @link https://tools.ietf.org/html/rfc2616#section-3.6.1
       
   851 	 * @param string $data Chunked body
       
   852 	 * @return string Decoded body
       
   853 	 */
       
   854 	protected static function decode_chunked($data) {
       
   855 		if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
       
   856 			return $data;
       
   857 		}
       
   858 
       
   859 		$decoded = '';
       
   860 		$encoded = $data;
       
   861 
       
   862 		while (true) {
       
   863 			$is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
       
   864 			if (!$is_chunked) {
       
   865 				// Looks like it's not chunked after all
       
   866 				return $data;
       
   867 			}
       
   868 
       
   869 			$length = hexdec(trim($matches[1]));
       
   870 			if ($length === 0) {
       
   871 				// Ignore trailer headers
       
   872 				return $decoded;
       
   873 			}
       
   874 
       
   875 			$chunk_length = strlen($matches[0]);
       
   876 			$decoded     .= substr($encoded, $chunk_length, $length);
       
   877 			$encoded      = substr($encoded, $chunk_length + $length + 2);
       
   878 
       
   879 			if (trim($encoded) === '0' || empty($encoded)) {
       
   880 				return $decoded;
       
   881 			}
       
   882 		}
       
   883 
       
   884 		// We'll never actually get down here
       
   885 		// @codeCoverageIgnoreStart
       
   886 	}
       
   887 	// @codeCoverageIgnoreEnd
       
   888 
       
   889 	/**
       
   890 	 * Convert a key => value array to a 'key: value' array for headers
       
   891 	 *
       
   892 	 * @param iterable $dictionary Dictionary of header values
       
   893 	 * @return array List of headers
       
   894 	 *
       
   895 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable.
       
   896 	 */
       
   897 	public static function flatten($dictionary) {
       
   898 		if (InputValidator::is_iterable($dictionary) === false) {
       
   899 			throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary));
       
   900 		}
       
   901 
       
   902 		$return = [];
       
   903 		foreach ($dictionary as $key => $value) {
       
   904 			$return[] = sprintf('%s: %s', $key, $value);
       
   905 		}
       
   906 
       
   907 		return $return;
       
   908 	}
       
   909 
       
   910 	/**
       
   911 	 * Decompress an encoded body
       
   912 	 *
       
   913 	 * Implements gzip, compress and deflate. Guesses which it is by attempting
       
   914 	 * to decode.
       
   915 	 *
       
   916 	 * @param string $data Compressed data in one of the above formats
       
   917 	 * @return string Decompressed string
       
   918 	 *
       
   919 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
       
   920 	 */
       
   921 	public static function decompress($data) {
       
   922 		if (is_string($data) === false) {
       
   923 			throw InvalidArgument::create(1, '$data', 'string', gettype($data));
       
   924 		}
       
   925 
       
   926 		if (trim($data) === '') {
       
   927 			// Empty body does not need further processing.
       
   928 			return $data;
       
   929 		}
       
   930 
       
   931 		$marker = substr($data, 0, 2);
       
   932 		if (!isset(self::$magic_compression_headers[$marker])) {
       
   933 			// Not actually compressed. Probably cURL ruining this for us.
       
   934 			return $data;
       
   935 		}
       
   936 
       
   937 		if (function_exists('gzdecode')) {
       
   938 			$decoded = @gzdecode($data);
       
   939 			if ($decoded !== false) {
       
   940 				return $decoded;
       
   941 			}
       
   942 		}
       
   943 
       
   944 		if (function_exists('gzinflate')) {
       
   945 			$decoded = @gzinflate($data);
       
   946 			if ($decoded !== false) {
       
   947 				return $decoded;
       
   948 			}
       
   949 		}
       
   950 
       
   951 		$decoded = self::compatible_gzinflate($data);
       
   952 		if ($decoded !== false) {
       
   953 			return $decoded;
       
   954 		}
       
   955 
       
   956 		if (function_exists('gzuncompress')) {
       
   957 			$decoded = @gzuncompress($data);
       
   958 			if ($decoded !== false) {
       
   959 				return $decoded;
       
   960 			}
       
   961 		}
       
   962 
       
   963 		return $data;
       
   964 	}
       
   965 
       
   966 	/**
       
   967 	 * Decompression of deflated string while staying compatible with the majority of servers.
       
   968 	 *
       
   969 	 * Certain Servers will return deflated data with headers which PHP's gzinflate()
       
   970 	 * function cannot handle out of the box. The following function has been created from
       
   971 	 * various snippets on the gzinflate() PHP documentation.
       
   972 	 *
       
   973 	 * Warning: Magic numbers within. Due to the potential different formats that the compressed
       
   974 	 * data may be returned in, some "magic offsets" are needed to ensure proper decompression
       
   975 	 * takes place. For a simple progmatic way to determine the magic offset in use, see:
       
   976 	 * https://core.trac.wordpress.org/ticket/18273
       
   977 	 *
       
   978 	 * @since 1.6.0
       
   979 	 * @link https://core.trac.wordpress.org/ticket/18273
       
   980 	 * @link https://www.php.net/gzinflate#70875
       
   981 	 * @link https://www.php.net/gzinflate#77336
       
   982 	 *
       
   983 	 * @param string $gz_data String to decompress.
       
   984 	 * @return string|bool False on failure.
       
   985 	 *
       
   986 	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
       
   987 	 */
       
   988 	public static function compatible_gzinflate($gz_data) {
       
   989 		if (is_string($gz_data) === false) {
       
   990 			throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data));
       
   991 		}
       
   992 
       
   993 		if (trim($gz_data) === '') {
       
   994 			return false;
       
   995 		}
       
   996 
       
   997 		// Compressed data might contain a full zlib header, if so strip it for
       
   998 		// gzinflate()
       
   999 		if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") {
       
  1000 			$i   = 10;
       
  1001 			$flg = ord(substr($gz_data, 3, 1));
       
  1002 			if ($flg > 0) {
       
  1003 				if ($flg & 4) {
       
  1004 					list($xlen) = unpack('v', substr($gz_data, $i, 2));
       
  1005 					$i         += 2 + $xlen;
       
  1006 				}
       
  1007 
       
  1008 				if ($flg & 8) {
       
  1009 					$i = strpos($gz_data, "\0", $i) + 1;
       
  1010 				}
       
  1011 
       
  1012 				if ($flg & 16) {
       
  1013 					$i = strpos($gz_data, "\0", $i) + 1;
       
  1014 				}
       
  1015 
       
  1016 				if ($flg & 2) {
       
  1017 					$i += 2;
       
  1018 				}
       
  1019 			}
       
  1020 
       
  1021 			$decompressed = self::compatible_gzinflate(substr($gz_data, $i));
       
  1022 			if ($decompressed !== false) {
       
  1023 				return $decompressed;
       
  1024 			}
       
  1025 		}
       
  1026 
       
  1027 		// If the data is Huffman Encoded, we must first strip the leading 2
       
  1028 		// byte Huffman marker for gzinflate()
       
  1029 		// The response is Huffman coded by many compressors such as
       
  1030 		// java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's
       
  1031 		// System.IO.Compression.DeflateStream.
       
  1032 		//
       
  1033 		// See https://decompres.blogspot.com/ for a quick explanation of this
       
  1034 		// data type
       
  1035 		$huffman_encoded = false;
       
  1036 
       
  1037 		// low nibble of first byte should be 0x08
       
  1038 		list(, $first_nibble) = unpack('h', $gz_data);
       
  1039 
       
  1040 		// First 2 bytes should be divisible by 0x1F
       
  1041 		list(, $first_two_bytes) = unpack('n', $gz_data);
       
  1042 
       
  1043 		if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) {
       
  1044 			$huffman_encoded = true;
       
  1045 		}
       
  1046 
       
  1047 		if ($huffman_encoded) {
       
  1048 			$decompressed = @gzinflate(substr($gz_data, 2));
       
  1049 			if ($decompressed !== false) {
       
  1050 				return $decompressed;
       
  1051 			}
       
  1052 		}
       
  1053 
       
  1054 		if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") {
       
  1055 			// ZIP file format header
       
  1056 			// Offset 6: 2 bytes, General-purpose field
       
  1057 			// Offset 26: 2 bytes, filename length
       
  1058 			// Offset 28: 2 bytes, optional field length
       
  1059 			// Offset 30: Filename field, followed by optional field, followed
       
  1060 			// immediately by data
       
  1061 			list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2));
       
  1062 
       
  1063 			// If the file has been compressed on the fly, 0x08 bit is set of
       
  1064 			// the general purpose field. We can use this to differentiate
       
  1065 			// between a compressed document, and a ZIP file
       
  1066 			$zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08);
       
  1067 
       
  1068 			if (!$zip_compressed_on_the_fly) {
       
  1069 				// Don't attempt to decode a compressed zip file
       
  1070 				return $gz_data;
       
  1071 			}
       
  1072 
       
  1073 			// Determine the first byte of data, based on the above ZIP header
       
  1074 			// offsets:
       
  1075 			$first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4)));
       
  1076 			$decompressed     = @gzinflate(substr($gz_data, 30 + $first_file_start));
       
  1077 			if ($decompressed !== false) {
       
  1078 				return $decompressed;
       
  1079 			}
       
  1080 
       
  1081 			return false;
       
  1082 		}
       
  1083 
       
  1084 		// Finally fall back to straight gzinflate
       
  1085 		$decompressed = @gzinflate($gz_data);
       
  1086 		if ($decompressed !== false) {
       
  1087 			return $decompressed;
       
  1088 		}
       
  1089 
       
  1090 		// Fallback for all above failing, not expected, but included for
       
  1091 		// debugging and preventing regressions and to track stats
       
  1092 		$decompressed = @gzinflate(substr($gz_data, 2));
       
  1093 		if ($decompressed !== false) {
       
  1094 			return $decompressed;
       
  1095 		}
       
  1096 
       
  1097 		return false;
       
  1098 	}
       
  1099 }