[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/Requests/src/ -> 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.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  }


Generated : Fri Apr 19 08:20:01 2024 Cross-referenced by PHPXref