[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu Nov 21 08:20:01 2024 | Cross-referenced by PHPXref |