[ Index ]

PHP Cross Reference of YOURLS

title

Body

[close]

/includes/vendor/rmccue/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.5';
 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       * @param string $url URL to request
 646       * @param array $headers Extra headers to send with the request
 647       * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
 648       * @param string $type HTTP request type
 649       * @param array $options Options for the request
 650       * @return void $options is updated with the results
 651       *
 652       * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL.
 653       */
 654  	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
 655          if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
 656              throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
 657          }
 658  
 659          if (empty($options['hooks'])) {
 660              $options['hooks'] = new Hooks();
 661          }
 662  
 663          if (is_array($options['auth'])) {
 664              $options['auth'] = new Basic($options['auth']);
 665          }
 666  
 667          if ($options['auth'] !== false) {
 668              $options['auth']->register($options['hooks']);
 669          }
 670  
 671          if (is_string($options['proxy']) || is_array($options['proxy'])) {
 672              $options['proxy'] = new Http($options['proxy']);
 673          }
 674  
 675          if ($options['proxy'] !== false) {
 676              $options['proxy']->register($options['hooks']);
 677          }
 678  
 679          if (is_array($options['cookies'])) {
 680              $options['cookies'] = new Jar($options['cookies']);
 681          } elseif (empty($options['cookies'])) {
 682              $options['cookies'] = new Jar();
 683          }
 684  
 685          if ($options['cookies'] !== false) {
 686              $options['cookies']->register($options['hooks']);
 687          }
 688  
 689          if ($options['idn'] !== false) {
 690              $iri       = new Iri($url);
 691              $iri->host = IdnaEncoder::encode($iri->ihost);
 692              $url       = $iri->uri;
 693          }
 694  
 695          // Massage the type to ensure we support it.
 696          $type = strtoupper($type);
 697  
 698          if (!isset($options['data_format'])) {
 699              if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) {
 700                  $options['data_format'] = 'query';
 701              } else {
 702                  $options['data_format'] = 'body';
 703              }
 704          }
 705      }
 706  
 707      /**
 708       * HTTP response parser
 709       *
 710       * @param string $headers Full response text including headers and body
 711       * @param string $url Original request URL
 712       * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
 713       * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
 714       * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
 715       * @return \WpOrg\Requests\Response
 716       *
 717       * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`)
 718       * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`)
 719       * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`)
 720       */
 721  	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
 722          $return = new Response();
 723          if (!$options['blocking']) {
 724              return $return;
 725          }
 726  
 727          $return->raw  = $headers;
 728          $return->url  = (string) $url;
 729          $return->body = '';
 730  
 731          if (!$options['filename']) {
 732              $pos = strpos($headers, "\r\n\r\n");
 733              if ($pos === false) {
 734                  // Crap!
 735                  throw new Exception('Missing header/body separator', 'requests.no_crlf_separator');
 736              }
 737  
 738              $headers = substr($return->raw, 0, $pos);
 739              // Headers will always be separated from the body by two new lines - `\n\r\n\r`.
 740              $body = substr($return->raw, $pos + 4);
 741              if (!empty($body)) {
 742                  $return->body = $body;
 743              }
 744          }
 745  
 746          // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
 747          $headers = str_replace("\r\n", "\n", $headers);
 748          // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
 749          $headers = preg_replace('/\n[ \t]/', ' ', $headers);
 750          $headers = explode("\n", $headers);
 751          preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
 752          if (empty($matches)) {
 753              throw new Exception('Response could not be parsed', 'noversion', $headers);
 754          }
 755  
 756          $return->protocol_version = (float) $matches[1];
 757          $return->status_code      = (int) $matches[2];
 758          if ($return->status_code >= 200 && $return->status_code < 300) {
 759              $return->success = true;
 760          }
 761  
 762          foreach ($headers as $header) {
 763              list($key, $value) = explode(':', $header, 2);
 764              $value             = trim($value);
 765              preg_replace('#(\s+)#i', ' ', $value);
 766              $return->headers[$key] = $value;
 767          }
 768  
 769          if (isset($return->headers['transfer-encoding'])) {
 770              $return->body = self::decode_chunked($return->body);
 771              unset($return->headers['transfer-encoding']);
 772          }
 773  
 774          if (isset($return->headers['content-encoding'])) {
 775              $return->body = self::decompress($return->body);
 776          }
 777  
 778          //fsockopen and cURL compatibility
 779          if (isset($return->headers['connection'])) {
 780              unset($return->headers['connection']);
 781          }
 782  
 783          $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]);
 784  
 785          if ($return->is_redirect() && $options['follow_redirects'] === true) {
 786              if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
 787                  if ($return->status_code === 303) {
 788                      $options['type'] = self::GET;
 789                  }
 790  
 791                  $options['redirected']++;
 792                  $location = $return->headers['location'];
 793                  if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
 794                      // relative redirect, for compatibility make it absolute
 795                      $location = Iri::absolutize($url, $location);
 796                      $location = $location->uri;
 797                  }
 798  
 799                  $hook_args = [
 800                      &$location,
 801                      &$req_headers,
 802                      &$req_data,
 803                      &$options,
 804                      $return,
 805                  ];
 806                  $options['hooks']->dispatch('requests.before_redirect', $hook_args);
 807                  $redirected            = self::request($location, $req_headers, $req_data, $options['type'], $options);
 808                  $redirected->history[] = $return;
 809                  return $redirected;
 810              } elseif ($options['redirected'] >= $options['redirects']) {
 811                  throw new Exception('Too many redirects', 'toomanyredirects', $return);
 812              }
 813          }
 814  
 815          $return->redirects = $options['redirected'];
 816  
 817          $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]);
 818          return $return;
 819      }
 820  
 821      /**
 822       * Callback for `transport.internal.parse_response`
 823       *
 824       * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response
 825       * while still executing a multiple request.
 826       *
 827       * @param string $response Full response text including headers and body (will be overwritten with Response instance)
 828       * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()}
 829       * @return void `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object
 830       */
 831  	public static function parse_multiple(&$response, $request) {
 832          try {
 833              $url      = $request['url'];
 834              $headers  = $request['headers'];
 835              $data     = $request['data'];
 836              $options  = $request['options'];
 837              $response = self::parse_response($response, $url, $headers, $data, $options);
 838          } catch (Exception $e) {
 839              $response = $e;
 840          }
 841      }
 842  
 843      /**
 844       * Decoded a chunked body as per RFC 2616
 845       *
 846       * @link https://tools.ietf.org/html/rfc2616#section-3.6.1
 847       * @param string $data Chunked body
 848       * @return string Decoded body
 849       */
 850  	protected static function decode_chunked($data) {
 851          if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
 852              return $data;
 853          }
 854  
 855          $decoded = '';
 856          $encoded = $data;
 857  
 858          while (true) {
 859              $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
 860              if (!$is_chunked) {
 861                  // Looks like it's not chunked after all
 862                  return $data;
 863              }
 864  
 865              $length = hexdec(trim($matches[1]));
 866              if ($length === 0) {
 867                  // Ignore trailer headers
 868                  return $decoded;
 869              }
 870  
 871              $chunk_length = strlen($matches[0]);
 872              $decoded     .= substr($encoded, $chunk_length, $length);
 873              $encoded      = substr($encoded, $chunk_length + $length + 2);
 874  
 875              if (trim($encoded) === '0' || empty($encoded)) {
 876                  return $decoded;
 877              }
 878          }
 879  
 880          // We'll never actually get down here
 881          // @codeCoverageIgnoreStart
 882      }
 883      // @codeCoverageIgnoreEnd
 884  
 885      /**
 886       * Convert a key => value array to a 'key: value' array for headers
 887       *
 888       * @param iterable $dictionary Dictionary of header values
 889       * @return array List of headers
 890       *
 891       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable.
 892       */
 893  	public static function flatten($dictionary) {
 894          if (InputValidator::is_iterable($dictionary) === false) {
 895              throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary));
 896          }
 897  
 898          $return = [];
 899          foreach ($dictionary as $key => $value) {
 900              $return[] = sprintf('%s: %s', $key, $value);
 901          }
 902  
 903          return $return;
 904      }
 905  
 906      /**
 907       * Decompress an encoded body
 908       *
 909       * Implements gzip, compress and deflate. Guesses which it is by attempting
 910       * to decode.
 911       *
 912       * @param string $data Compressed data in one of the above formats
 913       * @return string Decompressed string
 914       *
 915       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
 916       */
 917  	public static function decompress($data) {
 918          if (is_string($data) === false) {
 919              throw InvalidArgument::create(1, '$data', 'string', gettype($data));
 920          }
 921  
 922          if (trim($data) === '') {
 923              // Empty body does not need further processing.
 924              return $data;
 925          }
 926  
 927          $marker = substr($data, 0, 2);
 928          if (!isset(self::$magic_compression_headers[$marker])) {
 929              // Not actually compressed. Probably cURL ruining this for us.
 930              return $data;
 931          }
 932  
 933          if (function_exists('gzdecode')) {
 934              $decoded = @gzdecode($data);
 935              if ($decoded !== false) {
 936                  return $decoded;
 937              }
 938          }
 939  
 940          if (function_exists('gzinflate')) {
 941              $decoded = @gzinflate($data);
 942              if ($decoded !== false) {
 943                  return $decoded;
 944              }
 945          }
 946  
 947          $decoded = self::compatible_gzinflate($data);
 948          if ($decoded !== false) {
 949              return $decoded;
 950          }
 951  
 952          if (function_exists('gzuncompress')) {
 953              $decoded = @gzuncompress($data);
 954              if ($decoded !== false) {
 955                  return $decoded;
 956              }
 957          }
 958  
 959          return $data;
 960      }
 961  
 962      /**
 963       * Decompression of deflated string while staying compatible with the majority of servers.
 964       *
 965       * Certain Servers will return deflated data with headers which PHP's gzinflate()
 966       * function cannot handle out of the box. The following function has been created from
 967       * various snippets on the gzinflate() PHP documentation.
 968       *
 969       * Warning: Magic numbers within. Due to the potential different formats that the compressed
 970       * data may be returned in, some "magic offsets" are needed to ensure proper decompression
 971       * takes place. For a simple progmatic way to determine the magic offset in use, see:
 972       * https://core.trac.wordpress.org/ticket/18273
 973       *
 974       * @since 1.6.0
 975       * @link https://core.trac.wordpress.org/ticket/18273
 976       * @link https://www.php.net/gzinflate#70875
 977       * @link https://www.php.net/gzinflate#77336
 978       *
 979       * @param string $gz_data String to decompress.
 980       * @return string|bool False on failure.
 981       *
 982       * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
 983       */
 984  	public static function compatible_gzinflate($gz_data) {
 985          if (is_string($gz_data) === false) {
 986              throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data));
 987          }
 988  
 989          if (trim($gz_data) === '') {
 990              return false;
 991          }
 992  
 993          // Compressed data might contain a full zlib header, if so strip it for
 994          // gzinflate()
 995          if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") {
 996              $i   = 10;
 997              $flg = ord(substr($gz_data, 3, 1));
 998              if ($flg > 0) {
 999                  if ($flg & 4) {
1000                      list($xlen) = unpack('v', substr($gz_data, $i, 2));
1001                      $i         += 2 + $xlen;
1002                  }
1003  
1004                  if ($flg & 8) {
1005                      $i = strpos($gz_data, "\0", $i) + 1;
1006                  }
1007  
1008                  if ($flg & 16) {
1009                      $i = strpos($gz_data, "\0", $i) + 1;
1010                  }
1011  
1012                  if ($flg & 2) {
1013                      $i += 2;
1014                  }
1015              }
1016  
1017              $decompressed = self::compatible_gzinflate(substr($gz_data, $i));
1018              if ($decompressed !== false) {
1019                  return $decompressed;
1020              }
1021          }
1022  
1023          // If the data is Huffman Encoded, we must first strip the leading 2
1024          // byte Huffman marker for gzinflate()
1025          // The response is Huffman coded by many compressors such as
1026          // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's
1027          // System.IO.Compression.DeflateStream.
1028          //
1029          // See https://decompres.blogspot.com/ for a quick explanation of this
1030          // data type
1031          $huffman_encoded = false;
1032  
1033          // low nibble of first byte should be 0x08
1034          list(, $first_nibble) = unpack('h', $gz_data);
1035  
1036          // First 2 bytes should be divisible by 0x1F
1037          list(, $first_two_bytes) = unpack('n', $gz_data);
1038  
1039          if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) {
1040              $huffman_encoded = true;
1041          }
1042  
1043          if ($huffman_encoded) {
1044              $decompressed = @gzinflate(substr($gz_data, 2));
1045              if ($decompressed !== false) {
1046                  return $decompressed;
1047              }
1048          }
1049  
1050          if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") {
1051              // ZIP file format header
1052              // Offset 6: 2 bytes, General-purpose field
1053              // Offset 26: 2 bytes, filename length
1054              // Offset 28: 2 bytes, optional field length
1055              // Offset 30: Filename field, followed by optional field, followed
1056              // immediately by data
1057              list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2));
1058  
1059              // If the file has been compressed on the fly, 0x08 bit is set of
1060              // the general purpose field. We can use this to differentiate
1061              // between a compressed document, and a ZIP file
1062              $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08);
1063  
1064              if (!$zip_compressed_on_the_fly) {
1065                  // Don't attempt to decode a compressed zip file
1066                  return $gz_data;
1067              }
1068  
1069              // Determine the first byte of data, based on the above ZIP header
1070              // offsets:
1071              $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4)));
1072              $decompressed     = @gzinflate(substr($gz_data, 30 + $first_file_start));
1073              if ($decompressed !== false) {
1074                  return $decompressed;
1075              }
1076  
1077              return false;
1078          }
1079  
1080          // Finally fall back to straight gzinflate
1081          $decompressed = @gzinflate($gz_data);
1082          if ($decompressed !== false) {
1083              return $decompressed;
1084          }
1085  
1086          // Fallback for all above failing, not expected, but included for
1087          // debugging and preventing regressions and to track stats
1088          $decompressed = @gzinflate(substr($gz_data, 2));
1089          if ($decompressed !== false) {
1090              return $decompressed;
1091          }
1092  
1093          return false;
1094      }
1095  }


Generated: Thu Sep 19 05:10:04 2024 Cross-referenced by PHPXref 0.7.1