[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/Requests/ -> Requests.php (source)

   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.0';
 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          $options = array_merge(self::get_default_options(), $options);
 451  
 452          self::set_defaults($url, $headers, $data, $type, $options);
 453  
 454          $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]);
 455  
 456          if (!empty($options['transport'])) {
 457              $transport = $options['transport'];
 458  
 459              if (is_string($options['transport'])) {
 460                  $transport = new $transport();
 461              }
 462          }
 463          else {
 464              $need_ssl     = (stripos($url, 'https://') === 0);
 465              $capabilities = [Capability::SSL => $need_ssl];
 466              $transport    = self::get_transport($capabilities);
 467          }
 468          $response = $transport->request($url, $headers, $data, $options);
 469  
 470          $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]);
 471  
 472          return self::parse_response($response, $url, $headers, $data, $options);
 473      }
 474  
 475      /**
 476       * Send multiple HTTP requests simultaneously
 477       *
 478       * The `$requests` parameter takes an associative or indexed array of
 479       * request fields. The key of each request can be used to match up the
 480       * request with the returned data, or with the request passed into your
 481       * `multiple.request.complete` callback.
 482       *
 483       * The request fields value is an associative array with the following keys:
 484       *
 485       * - `url`: Request URL Same as the `$url` parameter to
 486       *    {@see \WpOrg\Requests\Requests::request()}
 487       *    (string, required)
 488       * - `headers`: Associative array of header fields. Same as the `$headers`
 489       *    parameter to {@see \WpOrg\Requests\Requests::request()}
 490       *    (array, default: `array()`)
 491       * - `data`: Associative array of data fields or a string. Same as the
 492       *    `$data` parameter to {@see \WpOrg\Requests\Requests::request()}
 493       *    (array|string, default: `array()`)
 494       * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type`
 495       *    parameter to {@see \WpOrg\Requests\Requests::request()}
 496       *    (string, default: `\WpOrg\Requests\Requests::GET`)
 497       * - `cookies`: Associative array of cookie name to value, or cookie jar.
 498       *    (array|\WpOrg\Requests\Cookie\Jar)
 499       *
 500       * If the `$options` parameter is specified, individual requests will
 501       * inherit options from it. This can be used to use a single hooking system,
 502       * or set all the types to `\WpOrg\Requests\Requests::POST`, for example.
 503       *
 504       * In addition, the `$options` parameter takes the following global options:
 505       *
 506       * - `complete`: A callback for when a request is complete. Takes two
 507       *    parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the
 508       *    ID from the request array (Note: this can also be overridden on a
 509       *    per-request basis, although that's a little silly)
 510       *    (callback)
 511       *
 512       * @param array $requests Requests data (see description for more information)
 513       * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()})
 514       * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object)
 515       *
 516       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
 517       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
 518       */
 519  	public static function request_multiple($requests, $options = []) {
 520          if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
 521              throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
 522          }
 523  
 524          if (is_array($options) === false) {
 525              throw InvalidArgument::create(2, '$options', 'array', gettype($options));
 526          }
 527  
 528          $options = array_merge(self::get_default_options(true), $options);
 529  
 530          if (!empty($options['hooks'])) {
 531              $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
 532              if (!empty($options['complete'])) {
 533                  $options['hooks']->register('multiple.request.complete', $options['complete']);
 534              }
 535          }
 536  
 537          foreach ($requests as $id => &$request) {
 538              if (!isset($request['headers'])) {
 539                  $request['headers'] = [];
 540              }
 541              if (!isset($request['data'])) {
 542                  $request['data'] = [];
 543              }
 544              if (!isset($request['type'])) {
 545                  $request['type'] = self::GET;
 546              }
 547              if (!isset($request['options'])) {
 548                  $request['options']         = $options;
 549                  $request['options']['type'] = $request['type'];
 550              }
 551              else {
 552                  if (empty($request['options']['type'])) {
 553                      $request['options']['type'] = $request['type'];
 554                  }
 555                  $request['options'] = array_merge($options, $request['options']);
 556              }
 557  
 558              self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);
 559  
 560              // Ensure we only hook in once
 561              if ($request['options']['hooks'] !== $options['hooks']) {
 562                  $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
 563                  if (!empty($request['options']['complete'])) {
 564                      $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
 565                  }
 566              }
 567          }
 568          unset($request);
 569  
 570          if (!empty($options['transport'])) {
 571              $transport = $options['transport'];
 572  
 573              if (is_string($options['transport'])) {
 574                  $transport = new $transport();
 575              }
 576          }
 577          else {
 578              $transport = self::get_transport();
 579          }
 580          $responses = $transport->request_multiple($requests, $options);
 581  
 582          foreach ($responses as $id => &$response) {
 583              // If our hook got messed with somehow, ensure we end up with the
 584              // correct response
 585              if (is_string($response)) {
 586                  $request = $requests[$id];
 587                  self::parse_multiple($response, $request);
 588                  $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]);
 589              }
 590          }
 591  
 592          return $responses;
 593      }
 594  
 595      /**
 596       * Get the default options
 597       *
 598       * @see \WpOrg\Requests\Requests::request() for values returned by this method
 599       * @param boolean $multirequest Is this a multirequest?
 600       * @return array Default option values
 601       */
 602  	protected static function get_default_options($multirequest = false) {
 603          $defaults           = static::OPTION_DEFAULTS;
 604          $defaults['verify'] = self::$certificate_path;
 605  
 606          if ($multirequest !== false) {
 607              $defaults['complete'] = null;
 608          }
 609          return $defaults;
 610      }
 611  
 612      /**
 613       * Get default certificate path.
 614       *
 615       * @return string Default certificate path.
 616       */
 617  	public static function get_certificate_path() {
 618          return self::$certificate_path;
 619      }
 620  
 621      /**
 622       * Set default certificate path.
 623       *
 624       * @param string|Stringable|bool $path Certificate path, pointing to a PEM file.
 625       *
 626       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean.
 627       */
 628  	public static function set_certificate_path($path) {
 629          if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) {
 630              throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path));
 631          }
 632  
 633          self::$certificate_path = $path;
 634      }
 635  
 636      /**
 637       * Set the default values
 638       *
 639       * @param string $url URL to request
 640       * @param array $headers Extra headers to send with the request
 641       * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
 642       * @param string $type HTTP request type
 643       * @param array $options Options for the request
 644       * @return void $options is updated with the results
 645       *
 646       * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL.
 647       */
 648  	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
 649          if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
 650              throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
 651          }
 652  
 653          if (empty($options['hooks'])) {
 654              $options['hooks'] = new Hooks();
 655          }
 656  
 657          if (is_array($options['auth'])) {
 658              $options['auth'] = new Basic($options['auth']);
 659          }
 660          if ($options['auth'] !== false) {
 661              $options['auth']->register($options['hooks']);
 662          }
 663  
 664          if (is_string($options['proxy']) || is_array($options['proxy'])) {
 665              $options['proxy'] = new Http($options['proxy']);
 666          }
 667          if ($options['proxy'] !== false) {
 668              $options['proxy']->register($options['hooks']);
 669          }
 670  
 671          if (is_array($options['cookies'])) {
 672              $options['cookies'] = new Jar($options['cookies']);
 673          }
 674          elseif (empty($options['cookies'])) {
 675              $options['cookies'] = new Jar();
 676          }
 677          if ($options['cookies'] !== false) {
 678              $options['cookies']->register($options['hooks']);
 679          }
 680  
 681          if ($options['idn'] !== false) {
 682              $iri       = new Iri($url);
 683              $iri->host = IdnaEncoder::encode($iri->ihost);
 684              $url       = $iri->uri;
 685          }
 686  
 687          // Massage the type to ensure we support it.
 688          $type = strtoupper($type);
 689  
 690          if (!isset($options['data_format'])) {
 691              if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) {
 692                  $options['data_format'] = 'query';
 693              }
 694              else {
 695                  $options['data_format'] = 'body';
 696              }
 697          }
 698      }
 699  
 700      /**
 701       * HTTP response parser
 702       *
 703       * @param string $headers Full response text including headers and body
 704       * @param string $url Original request URL
 705       * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
 706       * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
 707       * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
 708       * @return \WpOrg\Requests\Response
 709       *
 710       * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`)
 711       * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`)
 712       * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`)
 713       */
 714  	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
 715          $return = new Response();
 716          if (!$options['blocking']) {
 717              return $return;
 718          }
 719  
 720          $return->raw  = $headers;
 721          $return->url  = (string) $url;
 722          $return->body = '';
 723  
 724          if (!$options['filename']) {
 725              $pos = strpos($headers, "\r\n\r\n");
 726              if ($pos === false) {
 727                  // Crap!
 728                  throw new Exception('Missing header/body separator', 'requests.no_crlf_separator');
 729              }
 730  
 731              $headers = substr($return->raw, 0, $pos);
 732              // Headers will always be separated from the body by two new lines - `\n\r\n\r`.
 733              $body = substr($return->raw, $pos + 4);
 734              if (!empty($body)) {
 735                  $return->body = $body;
 736              }
 737          }
 738          // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
 739          $headers = str_replace("\r\n", "\n", $headers);
 740          // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
 741          $headers = preg_replace('/\n[ \t]/', ' ', $headers);
 742          $headers = explode("\n", $headers);
 743          preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
 744          if (empty($matches)) {
 745              throw new Exception('Response could not be parsed', 'noversion', $headers);
 746          }
 747          $return->protocol_version = (float) $matches[1];
 748          $return->status_code      = (int) $matches[2];
 749          if ($return->status_code >= 200 && $return->status_code < 300) {
 750              $return->success = true;
 751          }
 752  
 753          foreach ($headers as $header) {
 754              list($key, $value) = explode(':', $header, 2);
 755              $value             = trim($value);
 756              preg_replace('#(\s+)#i', ' ', $value);
 757              $return->headers[$key] = $value;
 758          }
 759          if (isset($return->headers['transfer-encoding'])) {
 760              $return->body = self::decode_chunked($return->body);
 761              unset($return->headers['transfer-encoding']);
 762          }
 763          if (isset($return->headers['content-encoding'])) {
 764              $return->body = self::decompress($return->body);
 765          }
 766  
 767          //fsockopen and cURL compatibility
 768          if (isset($return->headers['connection'])) {
 769              unset($return->headers['connection']);
 770          }
 771  
 772          $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]);
 773  
 774          if ($return->is_redirect() && $options['follow_redirects'] === true) {
 775              if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
 776                  if ($return->status_code === 303) {
 777                      $options['type'] = self::GET;
 778                  }
 779                  $options['redirected']++;
 780                  $location = $return->headers['location'];
 781                  if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
 782                      // relative redirect, for compatibility make it absolute
 783                      $location = Iri::absolutize($url, $location);
 784                      $location = $location->uri;
 785                  }
 786  
 787                  $hook_args = [
 788                      &$location,
 789                      &$req_headers,
 790                      &$req_data,
 791                      &$options,
 792                      $return,
 793                  ];
 794                  $options['hooks']->dispatch('requests.before_redirect', $hook_args);
 795                  $redirected            = self::request($location, $req_headers, $req_data, $options['type'], $options);
 796                  $redirected->history[] = $return;
 797                  return $redirected;
 798              }
 799              elseif ($options['redirected'] >= $options['redirects']) {
 800                  throw new Exception('Too many redirects', 'toomanyredirects', $return);
 801              }
 802          }
 803  
 804          $return->redirects = $options['redirected'];
 805  
 806          $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]);
 807          return $return;
 808      }
 809  
 810      /**
 811       * Callback for `transport.internal.parse_response`
 812       *
 813       * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response
 814       * while still executing a multiple request.
 815       *
 816       * @param string $response Full response text including headers and body (will be overwritten with Response instance)
 817       * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()}
 818       * @return void `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object
 819       */
 820  	public static function parse_multiple(&$response, $request) {
 821          try {
 822              $url      = $request['url'];
 823              $headers  = $request['headers'];
 824              $data     = $request['data'];
 825              $options  = $request['options'];
 826              $response = self::parse_response($response, $url, $headers, $data, $options);
 827          }
 828          catch (Exception $e) {
 829              $response = $e;
 830          }
 831      }
 832  
 833      /**
 834       * Decoded a chunked body as per RFC 2616
 835       *
 836       * @link https://tools.ietf.org/html/rfc2616#section-3.6.1
 837       * @param string $data Chunked body
 838       * @return string Decoded body
 839       */
 840  	protected static function decode_chunked($data) {
 841          if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
 842              return $data;
 843          }
 844  
 845          $decoded = '';
 846          $encoded = $data;
 847  
 848          while (true) {
 849              $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
 850              if (!$is_chunked) {
 851                  // Looks like it's not chunked after all
 852                  return $data;
 853              }
 854  
 855              $length = hexdec(trim($matches[1]));
 856              if ($length === 0) {
 857                  // Ignore trailer headers
 858                  return $decoded;
 859              }
 860  
 861              $chunk_length = strlen($matches[0]);
 862              $decoded     .= substr($encoded, $chunk_length, $length);
 863              $encoded      = substr($encoded, $chunk_length + $length + 2);
 864  
 865              if (trim($encoded) === '0' || empty($encoded)) {
 866                  return $decoded;
 867              }
 868          }
 869  
 870          // We'll never actually get down here
 871          // @codeCoverageIgnoreStart
 872      }
 873      // @codeCoverageIgnoreEnd
 874  
 875      /**
 876       * Convert a key => value array to a 'key: value' array for headers
 877       *
 878       * @param iterable $dictionary Dictionary of header values
 879       * @return array List of headers
 880       *
 881       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable.
 882       */
 883  	public static function flatten($dictionary) {
 884          if (InputValidator::is_iterable($dictionary) === false) {
 885              throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary));
 886          }
 887  
 888          $return = [];
 889          foreach ($dictionary as $key => $value) {
 890              $return[] = sprintf('%s: %s', $key, $value);
 891          }
 892          return $return;
 893      }
 894  
 895      /**
 896       * Decompress an encoded body
 897       *
 898       * Implements gzip, compress and deflate. Guesses which it is by attempting
 899       * to decode.
 900       *
 901       * @param string $data Compressed data in one of the above formats
 902       * @return string Decompressed string
 903       *
 904       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
 905       */
 906  	public static function decompress($data) {
 907          if (is_string($data) === false) {
 908              throw InvalidArgument::create(1, '$data', 'string', gettype($data));
 909          }
 910  
 911          if (trim($data) === '') {
 912              // Empty body does not need further processing.
 913              return $data;
 914          }
 915  
 916          $marker = substr($data, 0, 2);
 917          if (!isset(self::$magic_compression_headers[$marker])) {
 918              // Not actually compressed. Probably cURL ruining this for us.
 919              return $data;
 920          }
 921  
 922          if (function_exists('gzdecode')) {
 923              $decoded = @gzdecode($data);
 924              if ($decoded !== false) {
 925                  return $decoded;
 926              }
 927          }
 928  
 929          if (function_exists('gzinflate')) {
 930              $decoded = @gzinflate($data);
 931              if ($decoded !== false) {
 932                  return $decoded;
 933              }
 934          }
 935  
 936          $decoded = self::compatible_gzinflate($data);
 937          if ($decoded !== false) {
 938              return $decoded;
 939          }
 940  
 941          if (function_exists('gzuncompress')) {
 942              $decoded = @gzuncompress($data);
 943              if ($decoded !== false) {
 944                  return $decoded;
 945              }
 946          }
 947  
 948          return $data;
 949      }
 950  
 951      /**
 952       * Decompression of deflated string while staying compatible with the majority of servers.
 953       *
 954       * Certain Servers will return deflated data with headers which PHP's gzinflate()
 955       * function cannot handle out of the box. The following function has been created from
 956       * various snippets on the gzinflate() PHP documentation.
 957       *
 958       * Warning: Magic numbers within. Due to the potential different formats that the compressed
 959       * data may be returned in, some "magic offsets" are needed to ensure proper decompression
 960       * takes place. For a simple progmatic way to determine the magic offset in use, see:
 961       * https://core.trac.wordpress.org/ticket/18273
 962       *
 963       * @since 1.6.0
 964       * @link https://core.trac.wordpress.org/ticket/18273
 965       * @link https://www.php.net/gzinflate#70875
 966       * @link https://www.php.net/gzinflate#77336
 967       *
 968       * @param string $gz_data String to decompress.
 969       * @return string|bool False on failure.
 970       *
 971       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
 972       */
 973  	public static function compatible_gzinflate($gz_data) {
 974          if (is_string($gz_data) === false) {
 975              throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data));
 976          }
 977  
 978          if (trim($gz_data) === '') {
 979              return false;
 980          }
 981  
 982          // Compressed data might contain a full zlib header, if so strip it for
 983          // gzinflate()
 984          if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") {
 985              $i   = 10;
 986              $flg = ord(substr($gz_data, 3, 1));
 987              if ($flg > 0) {
 988                  if ($flg & 4) {
 989                      list($xlen) = unpack('v', substr($gz_data, $i, 2));
 990                      $i         += 2 + $xlen;
 991                  }
 992                  if ($flg & 8) {
 993                      $i = strpos($gz_data, "\0", $i) + 1;
 994                  }
 995                  if ($flg & 16) {
 996                      $i = strpos($gz_data, "\0", $i) + 1;
 997                  }
 998                  if ($flg & 2) {
 999                      $i += 2;
1000                  }
1001              }
1002              $decompressed = self::compatible_gzinflate(substr($gz_data, $i));
1003              if ($decompressed !== false) {
1004                  return $decompressed;
1005              }
1006          }
1007  
1008          // If the data is Huffman Encoded, we must first strip the leading 2
1009          // byte Huffman marker for gzinflate()
1010          // The response is Huffman coded by many compressors such as
1011          // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's
1012          // System.IO.Compression.DeflateStream.
1013          //
1014          // See https://decompres.blogspot.com/ for a quick explanation of this
1015          // data type
1016          $huffman_encoded = false;
1017  
1018          // low nibble of first byte should be 0x08
1019          list(, $first_nibble) = unpack('h', $gz_data);
1020  
1021          // First 2 bytes should be divisible by 0x1F
1022          list(, $first_two_bytes) = unpack('n', $gz_data);
1023  
1024          if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) {
1025              $huffman_encoded = true;
1026          }
1027  
1028          if ($huffman_encoded) {
1029              $decompressed = @gzinflate(substr($gz_data, 2));
1030              if ($decompressed !== false) {
1031                  return $decompressed;
1032              }
1033          }
1034  
1035          if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") {
1036              // ZIP file format header
1037              // Offset 6: 2 bytes, General-purpose field
1038              // Offset 26: 2 bytes, filename length
1039              // Offset 28: 2 bytes, optional field length
1040              // Offset 30: Filename field, followed by optional field, followed
1041              // immediately by data
1042              list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2));
1043  
1044              // If the file has been compressed on the fly, 0x08 bit is set of
1045              // the general purpose field. We can use this to differentiate
1046              // between a compressed document, and a ZIP file
1047              $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08);
1048  
1049              if (!$zip_compressed_on_the_fly) {
1050                  // Don't attempt to decode a compressed zip file
1051                  return $gz_data;
1052              }
1053  
1054              // Determine the first byte of data, based on the above ZIP header
1055              // offsets:
1056              $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4)));
1057              $decompressed     = @gzinflate(substr($gz_data, 30 + $first_file_start));
1058              if ($decompressed !== false) {
1059                  return $decompressed;
1060              }
1061              return false;
1062          }
1063  
1064          // Finally fall back to straight gzinflate
1065          $decompressed = @gzinflate($gz_data);
1066          if ($decompressed !== false) {
1067              return $decompressed;
1068          }
1069  
1070          // Fallback for all above failing, not expected, but included for
1071          // debugging and preventing regressions and to track stats
1072          $decompressed = @gzinflate(substr($gz_data, 2));
1073          if ($decompressed !== false) {
1074              return $decompressed;
1075          }
1076  
1077          return false;
1078      }
1079  }


Generated : Mon Dec 6 08:20:01 2021 Cross-referenced by PHPXref