File manager - Edit - /home/rbwebsco/sidrafran2.rb-webs.com/Requests.zip
Back
PK �\�\��rw$ w$ src/Autoload.phpnu �[��� <?php /** * Autoloader for Requests for PHP. * * Include this file if you'd like to avoid having to create your own autoloader. * * @package Requests * @since 2.0.0 * * @codeCoverageIgnore */ namespace WpOrg\Requests; /* * Ensure the autoloader is only declared once. * This safeguard is in place as this is the typical entry point for this library * and this file being required unconditionally could easily cause * fatal "Class already declared" errors. */ if (class_exists('WpOrg\Requests\Autoload') === false) { /** * Autoloader for Requests for PHP. * * This autoloader supports the PSR-4 based Requests 2.0.0 classes in a case-sensitive manner * as the most common server OS-es are case-sensitive and the file names are in mixed case. * * For the PSR-0 Requests 1.x BC-layer, requested classes will be treated case-insensitively. * * @package Requests */ final class Autoload { /** * List of the old PSR-0 class names in lowercase as keys with their PSR-4 case-sensitive name as a value. * * @var array */ private static $deprecated_classes = [ // Interfaces. 'requests_auth' => '\WpOrg\Requests\Auth', 'requests_hooker' => '\WpOrg\Requests\HookManager', 'requests_proxy' => '\WpOrg\Requests\Proxy', 'requests_transport' => '\WpOrg\Requests\Transport', // Classes. 'requests_cookie' => '\WpOrg\Requests\Cookie', 'requests_exception' => '\WpOrg\Requests\Exception', 'requests_hooks' => '\WpOrg\Requests\Hooks', 'requests_idnaencoder' => '\WpOrg\Requests\IdnaEncoder', 'requests_ipv6' => '\WpOrg\Requests\Ipv6', 'requests_iri' => '\WpOrg\Requests\Iri', 'requests_response' => '\WpOrg\Requests\Response', 'requests_session' => '\WpOrg\Requests\Session', 'requests_ssl' => '\WpOrg\Requests\Ssl', 'requests_auth_basic' => '\WpOrg\Requests\Auth\Basic', 'requests_cookie_jar' => '\WpOrg\Requests\Cookie\Jar', 'requests_proxy_http' => '\WpOrg\Requests\Proxy\Http', 'requests_response_headers' => '\WpOrg\Requests\Response\Headers', 'requests_transport_curl' => '\WpOrg\Requests\Transport\Curl', 'requests_transport_fsockopen' => '\WpOrg\Requests\Transport\Fsockopen', 'requests_utility_caseinsensitivedictionary' => '\WpOrg\Requests\Utility\CaseInsensitiveDictionary', 'requests_utility_filterediterator' => '\WpOrg\Requests\Utility\FilteredIterator', 'requests_exception_http' => '\WpOrg\Requests\Exception\Http', 'requests_exception_transport' => '\WpOrg\Requests\Exception\Transport', 'requests_exception_transport_curl' => '\WpOrg\Requests\Exception\Transport\Curl', 'requests_exception_http_304' => '\WpOrg\Requests\Exception\Http\Status304', 'requests_exception_http_305' => '\WpOrg\Requests\Exception\Http\Status305', 'requests_exception_http_306' => '\WpOrg\Requests\Exception\Http\Status306', 'requests_exception_http_400' => '\WpOrg\Requests\Exception\Http\Status400', 'requests_exception_http_401' => '\WpOrg\Requests\Exception\Http\Status401', 'requests_exception_http_402' => '\WpOrg\Requests\Exception\Http\Status402', 'requests_exception_http_403' => '\WpOrg\Requests\Exception\Http\Status403', 'requests_exception_http_404' => '\WpOrg\Requests\Exception\Http\Status404', 'requests_exception_http_405' => '\WpOrg\Requests\Exception\Http\Status405', 'requests_exception_http_406' => '\WpOrg\Requests\Exception\Http\Status406', 'requests_exception_http_407' => '\WpOrg\Requests\Exception\Http\Status407', 'requests_exception_http_408' => '\WpOrg\Requests\Exception\Http\Status408', 'requests_exception_http_409' => '\WpOrg\Requests\Exception\Http\Status409', 'requests_exception_http_410' => '\WpOrg\Requests\Exception\Http\Status410', 'requests_exception_http_411' => '\WpOrg\Requests\Exception\Http\Status411', 'requests_exception_http_412' => '\WpOrg\Requests\Exception\Http\Status412', 'requests_exception_http_413' => '\WpOrg\Requests\Exception\Http\Status413', 'requests_exception_http_414' => '\WpOrg\Requests\Exception\Http\Status414', 'requests_exception_http_415' => '\WpOrg\Requests\Exception\Http\Status415', 'requests_exception_http_416' => '\WpOrg\Requests\Exception\Http\Status416', 'requests_exception_http_417' => '\WpOrg\Requests\Exception\Http\Status417', 'requests_exception_http_418' => '\WpOrg\Requests\Exception\Http\Status418', 'requests_exception_http_428' => '\WpOrg\Requests\Exception\Http\Status428', 'requests_exception_http_429' => '\WpOrg\Requests\Exception\Http\Status429', 'requests_exception_http_431' => '\WpOrg\Requests\Exception\Http\Status431', 'requests_exception_http_500' => '\WpOrg\Requests\Exception\Http\Status500', 'requests_exception_http_501' => '\WpOrg\Requests\Exception\Http\Status501', 'requests_exception_http_502' => '\WpOrg\Requests\Exception\Http\Status502', 'requests_exception_http_503' => '\WpOrg\Requests\Exception\Http\Status503', 'requests_exception_http_504' => '\WpOrg\Requests\Exception\Http\Status504', 'requests_exception_http_505' => '\WpOrg\Requests\Exception\Http\Status505', 'requests_exception_http_511' => '\WpOrg\Requests\Exception\Http\Status511', 'requests_exception_http_unknown' => '\WpOrg\Requests\Exception\Http\StatusUnknown', ]; /** * Register the autoloader. * * Note: the autoloader is *prepended* in the autoload queue. * This is done to ensure that the Requests 2.0 autoloader takes precedence * over a potentially (dependency-registered) Requests 1.x autoloader. * * @internal This method contains a safeguard against the autoloader being * registered multiple times. This safeguard uses a global constant to * (hopefully/in most cases) still function correctly, even if the * class would be renamed. * * @return void */ public static function register() { if (defined('REQUESTS_AUTOLOAD_REGISTERED') === false) { spl_autoload_register([self::class, 'load'], true); define('REQUESTS_AUTOLOAD_REGISTERED', true); } } /** * Autoloader. * * @param string $class_name Name of the class name to load. * * @return bool Whether a class was loaded or not. */ public static function load($class_name) { // Check that the class starts with "Requests" (PSR-0) or "WpOrg\Requests" (PSR-4). $psr_4_prefix_pos = strpos($class_name, 'WpOrg\\Requests\\'); if (stripos($class_name, 'Requests') !== 0 && $psr_4_prefix_pos !== 0) { return false; } $class_lower = strtolower($class_name); if ($class_lower === 'requests') { // Reference to the original PSR-0 Requests class. $file = dirname(__DIR__) . '/library/Requests.php'; } elseif ($psr_4_prefix_pos === 0) { // PSR-4 classname. $file = __DIR__ . '/' . strtr(substr($class_name, 15), '\\', '/') . '.php'; } if (isset($file) && file_exists($file)) { include $file; return true; } /* * Okay, so the class starts with "Requests", but we couldn't find the file. * If this is one of the deprecated/renamed PSR-0 classes being requested, * let's alias it to the new name and throw a deprecation notice. */ if (isset(self::$deprecated_classes[$class_lower])) { /* * Integrators who cannot yet upgrade to the PSR-4 class names can silence deprecations * by defining a `REQUESTS_SILENCE_PSR0_DEPRECATIONS` constant and setting it to `true`. * The constant needs to be defined before the first deprecated class is requested * via this autoloader. */ if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') || REQUESTS_SILENCE_PSR0_DEPRECATIONS !== true) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error( 'The PSR-0 `Requests_...` class names in the Requests library are deprecated.' . ' Switch to the PSR-4 `WpOrg\Requests\...` class names at your earliest convenience.', E_USER_DEPRECATED ); // Prevent the deprecation notice from being thrown twice. if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); } } // Create an alias and let the autoloader recursively kick in to load the PSR-4 class. return class_alias(self::$deprecated_classes[$class_lower], $class_name, true); } return false; } } } PK �\�\��Sϟ0 �0 src/IdnaEncoder.phpnu �[��� <?php namespace WpOrg\Requests; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Utility\InputValidator; /** * IDNA URL encoder * * Note: Not fully compliant, as nameprep does nothing yet. * * @package Requests\Utilities * * @link https://tools.ietf.org/html/rfc3490 IDNA specification * @link https://tools.ietf.org/html/rfc3492 Punycode/Bootstrap specification */ class IdnaEncoder { /** * ACE prefix used for IDNA * * @link https://tools.ietf.org/html/rfc3490#section-5 * @var string */ const ACE_PREFIX = 'xn--'; /** * Maximum length of a IDNA URL in ASCII. * * @see \WpOrg\Requests\IdnaEncoder::to_ascii() * * @since 2.0.0 * * @var int */ const MAX_LENGTH = 64; /**#@+ * Bootstrap constant for Punycode * * @link https://tools.ietf.org/html/rfc3492#section-5 * @var int */ const BOOTSTRAP_BASE = 36; const BOOTSTRAP_TMIN = 1; const BOOTSTRAP_TMAX = 26; const BOOTSTRAP_SKEW = 38; const BOOTSTRAP_DAMP = 700; const BOOTSTRAP_INITIAL_BIAS = 72; const BOOTSTRAP_INITIAL_N = 128; /**#@-*/ /** * Encode a hostname using Punycode * * @param string|Stringable $hostname Hostname * @return string Punycode-encoded hostname * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function encode($hostname) { if (InputValidator::is_string_or_stringable($hostname) === false) { throw InvalidArgument::create(1, '$hostname', 'string|Stringable', gettype($hostname)); } $parts = explode('.', $hostname); foreach ($parts as &$part) { $part = self::to_ascii($part); } return implode('.', $parts); } /** * Convert a UTF-8 text string to an ASCII string using Punycode * * @param string $text ASCII or UTF-8 string (max length 64 characters) * @return string ASCII string * * @throws \WpOrg\Requests\Exception Provided string longer than 64 ASCII characters (`idna.provided_too_long`) * @throws \WpOrg\Requests\Exception Prepared string longer than 64 ASCII characters (`idna.prepared_too_long`) * @throws \WpOrg\Requests\Exception Provided string already begins with xn-- (`idna.provided_is_prefixed`) * @throws \WpOrg\Requests\Exception Encoded string longer than 64 ASCII characters (`idna.encoded_too_long`) */ public static function to_ascii($text) { // Step 1: Check if the text is already ASCII if (self::is_ascii($text)) { // Skip to step 7 if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Provided string is too long', 'idna.provided_too_long', $text); } // Step 2: nameprep $text = self::nameprep($text); // Step 3: UseSTD3ASCIIRules is false, continue // Step 4: Check if it's ASCII now if (self::is_ascii($text)) { // Skip to step 7 /* * As the `nameprep()` method returns the original string, this code will never be reached until * that method is properly implemented. */ // @codeCoverageIgnoreStart if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Prepared string is too long', 'idna.prepared_too_long', $text); // @codeCoverageIgnoreEnd } // Step 5: Check ACE prefix if (strpos($text, self::ACE_PREFIX) === 0) { throw new Exception('Provided string begins with ACE prefix', 'idna.provided_is_prefixed', $text); } // Step 6: Encode with Punycode $text = self::punycode_encode($text); // Step 7: Prepend ACE prefix $text = self::ACE_PREFIX . $text; // Step 8: Check size if (strlen($text) < self::MAX_LENGTH) { return $text; } throw new Exception('Encoded string is too long', 'idna.encoded_too_long', $text); } /** * Check whether a given text string contains only ASCII characters * * @internal (Testing found regex was the fastest implementation) * * @param string $text Text to examine. * @return bool Is the text string ASCII-only? */ protected static function is_ascii($text) { return (preg_match('/(?:[^\x00-\x7F])/', $text) !== 1); } /** * Prepare a text string for use as an IDNA name * * @todo Implement this based on RFC 3491 and the newer 5891 * @param string $text Text to prepare. * @return string Prepared string */ protected static function nameprep($text) { return $text; } /** * Convert a UTF-8 string to a UCS-4 codepoint array * * Based on \WpOrg\Requests\Iri::replace_invalid_with_pct_encoding() * * @param string $input Text to convert. * @return array Unicode code points * * @throws \WpOrg\Requests\Exception Invalid UTF-8 codepoint (`idna.invalidcodepoint`) */ protected static function utf8_to_codepoints($input) { $codepoints = []; // Get number of bytes $strlen = strlen($input); // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer -- This is a deliberate choice. for ($position = 0; $position < $strlen; $position++) { $value = ord($input[$position]); if ((~$value & 0x80) === 0x80) { // One byte sequence: $character = $value; $length = 1; $remaining = 0; } elseif (($value & 0xE0) === 0xC0) { // Two byte sequence: $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } elseif (($value & 0xF0) === 0xE0) { // Three byte sequence: $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } elseif (($value & 0xF8) === 0xF0) { // Four byte sequence: $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } else { // Invalid byte: throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $value); } if ($remaining > 0) { if ($position + $length > $strlen) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } for ($position++; $remaining > 0; $position++) { $value = ord($input[$position]); // If it is invalid, count the sequence as invalid and reprocess the current byte: if (($value & 0xC0) !== 0x80) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } --$remaining; $character |= ($value & 0x3F) << ($remaining * 6); } $position--; } if (// Non-shortest form sequences are invalid ($length > 1 && $character <= 0x7F) || ($length > 2 && $character <= 0x7FF) || ($length > 3 && $character <= 0xFFFF) // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || ($character >= 0xFDD0 && $character <= 0xFDEF) || ( // Everything else not in ucschar ($character > 0xD7FF && $character < 0xF900) || $character < 0x20 || ($character > 0x7E && $character < 0xA0) || $character > 0xEFFFD ) ) { throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } $codepoints[] = $character; } return $codepoints; } /** * RFC3492-compliant encoder * * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code * * @param string $input UTF-8 encoded string to encode * @return string Punycode-encoded string * * @throws \WpOrg\Requests\Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) */ public static function punycode_encode($input) { $output = ''; // let n = initial_n $n = self::BOOTSTRAP_INITIAL_N; // let delta = 0 $delta = 0; // let bias = initial_bias $bias = self::BOOTSTRAP_INITIAL_BIAS; // let h = b = the number of basic code points in the input $h = 0; $b = 0; // see loop // copy them to the output in order $codepoints = self::utf8_to_codepoints($input); $extended = []; foreach ($codepoints as $char) { if ($char < 128) { // Character is valid ASCII // TODO: this should also check if it's valid for a URL $output .= chr($char); $h++; // Check if the character is non-ASCII, but below initial n // This never occurs for Punycode, so ignore in coverage // @codeCoverageIgnoreStart } elseif ($char < $n) { throw new Exception('Invalid character', 'idna.character_outside_domain', $char); // @codeCoverageIgnoreEnd } else { $extended[$char] = true; } } $extended = array_keys($extended); sort($extended); $b = $h; // [copy them] followed by a delimiter if b > 0 if (strlen($output) > 0) { $output .= '-'; } // {if the input contains a non-basic code point < n then fail} // while h < length(input) do begin $codepointcount = count($codepoints); while ($h < $codepointcount) { // let m = the minimum code point >= n in the input $m = array_shift($extended); //printf('next code point to insert is %s' . PHP_EOL, dechex($m)); // let delta = delta + (m - n) * (h + 1), fail on overflow $delta += ($m - $n) * ($h + 1); // let n = m $n = $m; // for each code point c in the input (in order) do begin for ($num = 0; $num < $codepointcount; $num++) { $c = $codepoints[$num]; // if c < n then increment delta, fail on overflow if ($c < $n) { $delta++; } elseif ($c === $n) { // if c == n then begin // let q = delta $q = $delta; // for k = base to infinity in steps of base do begin for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) { // let t = tmin if k <= bias {+ tmin}, or // tmax if k >= bias + tmax, or k - bias otherwise if ($k <= ($bias + self::BOOTSTRAP_TMIN)) { $t = self::BOOTSTRAP_TMIN; } elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { $t = self::BOOTSTRAP_TMAX; } else { $t = $k - $bias; } // if q < t then break if ($q < $t) { break; } // output the code point for digit t + ((q - t) mod (base - t)) $digit = (int) ($t + (($q - $t) % (self::BOOTSTRAP_BASE - $t))); $output .= self::digit_to_char($digit); // let q = (q - t) div (base - t) $q = (int) floor(($q - $t) / (self::BOOTSTRAP_BASE - $t)); } // end // output the code point for digit q $output .= self::digit_to_char($q); // let bias = adapt(delta, h + 1, test h equals b?) $bias = self::adapt($delta, $h + 1, $h === $b); // let delta = 0 $delta = 0; // increment h $h++; } // end } // end // increment delta and n $delta++; $n++; } // end return $output; } /** * Convert a digit to its respective character * * @link https://tools.ietf.org/html/rfc3492#section-5 * * @param int $digit Digit in the range 0-35 * @return string Single character corresponding to digit * * @throws \WpOrg\Requests\Exception On invalid digit (`idna.invalid_digit`) */ protected static function digit_to_char($digit) { // @codeCoverageIgnoreStart // As far as I know, this never happens, but still good to be sure. if ($digit < 0 || $digit > 35) { throw new Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); } // @codeCoverageIgnoreEnd $digits = 'abcdefghijklmnopqrstuvwxyz0123456789'; return substr($digits, $digit, 1); } /** * Adapt the bias * * @link https://tools.ietf.org/html/rfc3492#section-6.1 * @param int $delta * @param int $numpoints * @param bool $firsttime * @return int|float New bias * * function adapt(delta,numpoints,firsttime): */ protected static function adapt($delta, $numpoints, $firsttime) { // if firsttime then let delta = delta div damp if ($firsttime) { $delta = floor($delta / self::BOOTSTRAP_DAMP); } else { // else let delta = delta div 2 $delta = floor($delta / 2); } // let delta = delta + (delta div numpoints) $delta += floor($delta / $numpoints); // let k = 0 $k = 0; // while delta > ((base - tmin) * tmax) div 2 do begin $max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2); while ($delta > $max) { // let delta = delta div (base - tmin) $delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN)); // let k = k + base $k += self::BOOTSTRAP_BASE; } // end // return k + (((base - tmin + 1) * delta) div (delta + skew)) return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW)); } } PK �\�\,��S src/Transport.phpnu �[��� <?php /** * Base HTTP transport * * @package Requests\Transport */ namespace WpOrg\Requests; /** * Base HTTP transport * * @package Requests\Transport */ interface Transport { /** * Perform a request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result */ public function request($url, $headers = [], $data = [], $options = []); /** * Send multiple requests simultaneously * * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) */ public function request_multiple($requests, $options); /** * Self-test whether the transport can be used. * * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport can be used. */ public static function test($capabilities = []); } PK �\�\Q�ф ф src/Requests.phpnu �[��� <?php /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ namespace WpOrg\Requests; use WpOrg\Requests\Auth\Basic; use WpOrg\Requests\Capability; use WpOrg\Requests\Cookie\Jar; use WpOrg\Requests\Exception; use WpOrg\Requests\Exception\InvalidArgument; use WpOrg\Requests\Hooks; use WpOrg\Requests\IdnaEncoder; use WpOrg\Requests\Iri; use WpOrg\Requests\Proxy\Http; use WpOrg\Requests\Response; use WpOrg\Requests\Transport\Curl; use WpOrg\Requests\Transport\Fsockopen; use WpOrg\Requests\Utility\InputValidator; /** * Requests for PHP * * Inspired by Requests for Python. * * Based on concepts from SimplePie_File, RequestCore and WP_Http. * * @package Requests */ class Requests { /** * POST method * * @var string */ const POST = 'POST'; /** * PUT method * * @var string */ const PUT = 'PUT'; /** * GET method * * @var string */ const GET = 'GET'; /** * HEAD method * * @var string */ const HEAD = 'HEAD'; /** * DELETE method * * @var string */ const DELETE = 'DELETE'; /** * OPTIONS method * * @var string */ const OPTIONS = 'OPTIONS'; /** * TRACE method * * @var string */ const TRACE = 'TRACE'; /** * PATCH method * * @link https://tools.ietf.org/html/rfc5789 * @var string */ const PATCH = 'PATCH'; /** * Default size of buffer size to read streams * * @var integer */ const BUFFER_SIZE = 1160; /** * Option defaults. * * @see \WpOrg\Requests\Requests::get_default_options() * @see \WpOrg\Requests\Requests::request() for values returned by this method * * @since 2.0.0 * * @var array */ const OPTION_DEFAULTS = [ 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, 'blocking' => true, 'type' => self::GET, 'filename' => false, 'auth' => false, 'proxy' => false, 'cookies' => false, 'max_bytes' => false, 'idn' => true, 'hooks' => null, 'transport' => null, 'verify' => null, 'verifyname' => true, ]; /** * Default supported Transport classes. * * @since 2.0.0 * * @var array */ const DEFAULT_TRANSPORTS = [ Curl::class => Curl::class, Fsockopen::class => Fsockopen::class, ]; /** * Current version of Requests * * @var string */ const VERSION = '2.0.17'; /** * Selected transport name * * Use {@see \WpOrg\Requests\Requests::get_transport()} instead * * @var array */ public static $transport = []; /** * Registered transport classes * * @var array */ protected static $transports = []; /** * Default certificate path. * * @see \WpOrg\Requests\Requests::get_certificate_path() * @see \WpOrg\Requests\Requests::set_certificate_path() * * @var string */ protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem'; /** * All (known) valid deflate, gzip header magic markers. * * These markers relate to different compression levels. * * @link https://stackoverflow.com/a/43170354/482864 Marker source. * * @since 2.0.0 * * @var array */ private static $magic_compression_headers = [ "\x1f\x8b" => true, // Gzip marker. "\x78\x01" => true, // Zlib marker - level 1. "\x78\x5e" => true, // Zlib marker - level 2 to 5. "\x78\x9c" => true, // Zlib marker - level 6. "\x78\xda" => true, // Zlib marker - level 7 to 9. ]; /** * This is a static class, do not instantiate it * * @codeCoverageIgnore */ private function __construct() {} /** * Register a transport * * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface */ public static function add_transport($transport) { if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } self::$transports[$transport] = $transport; } /** * Get the fully qualified class name (FQCN) for a working transport. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return string FQCN of the transport to use, or an empty string if no transport was * found which provided the requested capabilities. */ protected static function get_transport_class(array $capabilities = []) { // Caching code, don't bother testing coverage. // @codeCoverageIgnoreStart // Array of capabilities as a string to be used as an array key. ksort($capabilities); $cap_string = serialize($capabilities); // Don't search for a transport if it's already been done for these $capabilities. if (isset(self::$transport[$cap_string])) { return self::$transport[$cap_string]; } // Ensure we will not run this same check again later on. self::$transport[$cap_string] = ''; // @codeCoverageIgnoreEnd if (empty(self::$transports)) { self::$transports = self::DEFAULT_TRANSPORTS; } // Find us a working transport. foreach (self::$transports as $class) { if (!class_exists($class)) { continue; } $result = $class::test($capabilities); if ($result === true) { self::$transport[$cap_string] = $class; break; } } return self::$transport[$cap_string]; } /** * Get a working transport. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return \WpOrg\Requests\Transport * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). */ protected static function get_transport(array $capabilities = []) { $class = self::get_transport_class($capabilities); if ($class === '') { throw new Exception('No working transports found', 'notransport', self::$transports); } return new $class(); } /** * Checks to see if we have a transport for the capabilities requested. * * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} * interface as constants. * * Example usage: * `Requests::has_capabilities([Capability::SSL => true])`. * * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`. * @return bool Whether the transport has the requested capabilities. */ public static function has_capabilities(array $capabilities = []) { return self::get_transport_class($capabilities) !== ''; } /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a GET request */ public static function get($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::GET, $options); } /** * Send a HEAD request */ public static function head($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::HEAD, $options); } /** * Send a DELETE request */ public static function delete($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::DELETE, $options); } /** * Send a TRACE request */ public static function trace($url, $headers = [], $options = []) { return self::request($url, $headers, null, self::TRACE, $options); } /**#@-*/ /**#@+ * @see \WpOrg\Requests\Requests::request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return \WpOrg\Requests\Response */ /** * Send a POST request */ public static function post($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request */ public static function put($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::PUT, $options); } /** * Send an OPTIONS request */ public static function options($url, $headers = [], $data = [], $options = []) { return self::request($url, $headers, $data, self::OPTIONS, $options); } /** * Send a PATCH request * * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public static function patch($url, $headers, $data = [], $options = []) { return self::request($url, $headers, $data, self::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * The `$options` parameter takes an associative array with the following * options: * * - `timeout`: How long should we wait for a response? * Note: for cURL, a minimum of 1 second applies, as DNS resolution * operates at second-resolution only. * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `connect_timeout`: How long should we wait while trying to connect? * (float, seconds with a millisecond precision, default: 10, example: 0.01) * - `useragent`: Useragent to send to the server * (string, default: php-requests/$version) * - `follow_redirects`: Should we follow 3xx redirects? * (boolean, default: true) * - `redirects`: How many times should we redirect before erroring? * (integer, default: 10) * - `blocking`: Should we block processing on this request? * (boolean, default: true) * - `filename`: File to stream the body to instead. * (string|boolean, default: false) * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (\WpOrg\Requests\Auth|array|boolean, default: false) * - `proxy`: Proxy details to use for proxy by-passing and authentication * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) * - `max_bytes`: Limit for the response body size. * (integer|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a * transport object. Defaults to the first working transport from * {@see \WpOrg\Requests\Requests::getTransport()} * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) * - `hooks`: Hooks handler. * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) * - `verify`: Should we verify SSL certificates? Allows passing in a custom * certificate file as a string. (Using true uses the system-wide root * certificate store instead, but this may have different behaviour * across transports.) * (string|boolean, default: certificates/cacert.pem) * - `verifyname`: Should we verify the common name in the SSL certificate? * (boolean, default: true) * - `data_format`: How should we send the `$data` parameter? * (string, one of 'query' or 'body', default: 'query' for * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) * * @param string|Stringable $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see description for more information) * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { if (InputValidator::is_string_or_stringable($url) === false) { throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); } if (is_string($type) === false) { throw InvalidArgument::create(4, '$type', 'string', gettype($type)); } if (is_array($options) === false) { throw InvalidArgument::create(5, '$options', 'array', gettype($options)); } if (empty($options['type'])) { $options['type'] = $type; } $options = array_merge(self::get_default_options(), $options); self::set_defaults($url, $headers, $data, $type, $options); $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (stripos($url, 'https://') === 0); $capabilities = [Capability::SSL => $need_ssl]; $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see \WpOrg\Requests\Requests::request()} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see \WpOrg\Requests\Requests::request()} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} * (array|string, default: `array()`) * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` * parameter to {@see \WpOrg\Requests\Requests::request()} * (string, default: `\WpOrg\Requests\Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|\WpOrg\Requests\Cookie\Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public static function request_multiple($requests, $options = []) { if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); } if (is_array($options) === false) { throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = []; } if (!isset($request['data'])) { $request['data'] = []; } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); } } return $responses; } /** * Get the default options * * @see \WpOrg\Requests\Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = static::OPTION_DEFAULTS; $defaults['verify'] = self::$certificate_path; if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { return self::$certificate_path; } /** * Set default certificate path. * * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. */ public static function set_certificate_path($path) { if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); } self::$certificate_path = $path; } /** * Set the default values * * The $options parameter is updated with the results. * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return void * * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Http($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Iri($url); $iri->host = IdnaEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return \WpOrg\Requests\Response * * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = (string) $url; $return->body = ''; if (!$options['filename']) { $pos = strpos($headers, "\r\n\r\n"); if ($pos === false) { // Crap! throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); // Headers will always be separated from the body by two new lines - `\n\r\n\r`. $body = substr($return->raw, $pos + 4); if (!empty($body)) { $return->body = $body; } } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Iri::absolutize($url, $location); $location = $location->uri; } $hook_args = [ &$location, &$req_headers, &$req_data, &$options, $return, ]; $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response * while still executing a multiple request. * * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} * @return void */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param iterable $dictionary Dictionary of header values * @return array List of headers * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. */ public static function flatten($dictionary) { if (InputValidator::is_iterable($dictionary) === false) { throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); } $return = []; foreach ($dictionary as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function decompress($data) { if (is_string($data) === false) { throw InvalidArgument::create(1, '$data', 'string', gettype($data)); } if (trim($data) === '') { // Empty body does not need further processing. return $data; } $marker = substr($data, 0, 2); if (!isset(self::$magic_compression_headers[$marker])) { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode')) { $decoded = @gzdecode($data); if ($decoded !== false) { return $decoded; } } if (function_exists('gzinflate')) { $decoded = @gzinflate($data); if ($decoded !== false) { return $decoded; } } $decoded = self::compatible_gzinflate($data); if ($decoded !== false) { return $decoded; } if (function_exists('gzuncompress')) { $decoded = @gzuncompress($data); if ($decoded !== false) { return $decoded; } } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 1.6.0 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://www.php.net/gzinflate#70875 * @link https://www.php.net/gzinflate#77336 * * @param string $gz_data String to decompress. * @return string|bool False on failure. * * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. */ public static function compatible_gzinflate($gz_data) { if (is_string($gz_data) === false) { throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); } if (trim($gz_data) === '') { return false; } // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gz_data, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gz_data, $i, 2)); $i += 2 + $xlen; } if ($flg & 8) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 2) { $i += 2; } } $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); if ($decompressed !== false) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gz_data); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gz_data); if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { $huffman_encoded = true; } if ($huffman_encoded) { $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } } if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gz_data; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); if ($decompressed !== false) { return $decompressed; } return false; } // Finally fall back to straight gzinflate $decompressed = @gzinflate($gz_data); if ($decompressed !== false) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } return false; } } PK �\�\�JM�<