diff -r 490d5cc509ed -r cf61fcea0001 wp/wp-includes/class-requests.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wp/wp-includes/class-requests.php Mon Oct 14 17:39:30 2019 +0200 @@ -0,0 +1,980 @@ +dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options)); + + if (!empty($options['transport'])) { + $transport = $options['transport']; + + if (is_string($options['transport'])) { + $transport = new $transport(); + } + } + else { + $need_ssl = (0 === stripos($url, 'https://')); + $capabilities = array('ssl' => $need_ssl); + $transport = self::get_transport($capabilities); + } + $response = $transport->request($url, $headers, $data, $options); + + $options['hooks']->dispatch('requests.before_parse', array(&$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 Requests::request} + * (string, required) + * - `headers`: Associative array of header fields. Same as the `$headers` + * parameter to {@see Requests::request} + * (array, default: `array()`) + * - `data`: Associative array of data fields or a string. Same as the + * `$data` parameter to {@see Requests::request} + * (array|string, default: `array()`) + * - `type`: HTTP request type (use Requests constants). Same as the `$type` + * parameter to {@see Requests::request} + * (string, default: `Requests::GET`) + * - `cookies`: Associative array of cookie name to value, or cookie jar. + * (array|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 `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 Requests_Response/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 Requests::request}) + * @return array Responses (either Requests_Response or a Requests_Exception object) + */ + public static function request_multiple($requests, $options = array()) { + $options = array_merge(self::get_default_options(true), $options); + + if (!empty($options['hooks'])) { + $options['hooks']->register('transport.internal.parse_response', array('Requests', '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'] = array(); + } + if (!isset($request['data'])) { + $request['data'] = array(); + } + 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', array('Requests', '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', array(&$response, $id)); + } + } + + return $responses; + } + + /** + * Get the default options + * + * @see 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 = array( + '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' => Requests::get_certificate_path(), + 'verifyname' => true, + ); + if ($multirequest !== false) { + $defaults['complete'] = null; + } + return $defaults; + } + + /** + * Get default certificate path. + * + * @return string Default certificate path. + */ + public static function get_certificate_path() { + if ( ! empty( Requests::$certificate_path ) ) { + return Requests::$certificate_path; + } + + return dirname(__FILE__) . '/Requests/Transport/cacert.pem'; + } + + /** + * Set default certificate path. + * + * @param string $path Certificate path, pointing to a PEM file. + */ + public static function set_certificate_path( $path ) { + Requests::$certificate_path = $path; + } + + /** + * Set the default values + * + * @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 array $options + */ + protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { + if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { + throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); + } + + if (empty($options['hooks'])) { + $options['hooks'] = new Requests_Hooks(); + } + + if (is_array($options['auth'])) { + $options['auth'] = new Requests_Auth_Basic($options['auth']); + } + if ($options['auth'] !== false) { + $options['auth']->register($options['hooks']); + } + + if (is_string($options['proxy']) || is_array($options['proxy'])) { + $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']); + } + if ($options['proxy'] !== false) { + $options['proxy']->register($options['hooks']); + } + + if (is_array($options['cookies'])) { + $options['cookies'] = new Requests_Cookie_Jar($options['cookies']); + } + elseif (empty($options['cookies'])) { + $options['cookies'] = new Requests_Cookie_Jar(); + } + if ($options['cookies'] !== false) { + $options['cookies']->register($options['hooks']); + } + + if ($options['idn'] !== false) { + $iri = new Requests_IRI($url); + $iri->host = Requests_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, array(self::HEAD, self::GET, self::DELETE))) { + $options['data_format'] = 'query'; + } + else { + $options['data_format'] = 'body'; + } + } + } + + /** + * HTTP response parser + * + * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`) + * @throws Requests_Exception On missing head/body separator (`noversion`) + * @throws Requests_Exception On missing head/body separator (`toomanyredirects`) + * + * @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 Requests_Response + */ + protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { + $return = new Requests_Response(); + if (!$options['blocking']) { + return $return; + } + + $return->raw = $headers; + $return->url = $url; + + if (!$options['filename']) { + if (($pos = strpos($headers, "\r\n\r\n")) === false) { + // Crap! + throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator'); + } + + $headers = substr($return->raw, 0, $pos); + $return->body = substr($return->raw, $pos + strlen("\n\r\n\r")); + } + else { + $return->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 Requests_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', array(&$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 = Requests_IRI::absolutize($url, $location); + $location = $location->uri; + } + + $hook_args = array( + &$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 Requests_Exception('Too many redirects', 'toomanyredirects', $return); + } + } + + $return->redirects = $options['redirected']; + + $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options)); + return $return; + } + + /** + * Callback for `transport.internal.parse_response` + * + * Internal use only. Converts a raw HTTP response to a Requests_Response + * while still executing a multiple request. + * + * @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 Requests::request_multiple()} + * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object + */ + 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 (Requests_Exception $e) { + $response = $e; + } + } + + /** + * Decoded a chunked body as per RFC 2616 + * + * @see 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 array $array Dictionary of header values + * @return array List of headers + */ + public static function flatten($array) { + $return = array(); + foreach ($array as $key => $value) { + $return[] = sprintf('%s: %s', $key, $value); + } + return $return; + } + + /** + * Convert a key => value array to a 'key: value' array for headers + * + * @codeCoverageIgnore + * @deprecated Misspelling of {@see Requests::flatten} + * @param array $array Dictionary of header values + * @return array List of headers + */ + public static function flattern($array) { + return self::flatten($array); + } + + /** + * 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 + */ + public static function decompress($data) { + if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") { + // Not actually compressed. Probably cURL ruining this for us. + return $data; + } + + if (function_exists('gzdecode') && ($decoded = @gzdecode($data)) !== false) { + return $decoded; + } + elseif (function_exists('gzinflate') && ($decoded = @gzinflate($data)) !== false) { + return $decoded; + } + elseif (($decoded = self::compatible_gzinflate($data)) !== false) { + return $decoded; + } + elseif (function_exists('gzuncompress') && ($decoded = @gzuncompress($data)) !== 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 2.8.1 + * @link https://core.trac.wordpress.org/ticket/18273 + * @link https://secure.php.net/manual/en/function.gzinflate.php#70875 + * @link https://secure.php.net/manual/en/function.gzinflate.php#77336 + * + * @param string $gzData String to decompress. + * @return string|bool False on failure. + */ + public static function compatible_gzinflate($gzData) { + // Compressed data might contain a full zlib header, if so strip it for + // gzinflate() + if (substr($gzData, 0, 3) == "\x1f\x8b\x08") { + $i = 10; + $flg = ord(substr($gzData, 3, 1)); + if ($flg > 0) { + if ($flg & 4) { + list($xlen) = unpack('v', substr($gzData, $i, 2)); + $i = $i + 2 + $xlen; + } + if ($flg & 8) { + $i = strpos($gzData, "\0", $i) + 1; + } + if ($flg & 16) { + $i = strpos($gzData, "\0", $i) + 1; + } + if ($flg & 2) { + $i = $i + 2; + } + } + $decompressed = self::compatible_gzinflate(substr($gzData, $i)); + if (false !== $decompressed) { + 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', $gzData); + + // First 2 bytes should be divisible by 0x1F + list(, $first_two_bytes) = unpack('n', $gzData); + + if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) { + $huffman_encoded = true; + } + + if ($huffman_encoded) { + if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { + return $decompressed; + } + } + + if ("\x50\x4b\x03\x04" == substr($gzData, 0, 4)) { + // 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($gzData, 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 == (0x08 & $general_purpose_flag)); + + if (!$zip_compressed_on_the_fly) { + // Don't attempt to decode a compressed zip file + return $gzData; + } + + // Determine the first byte of data, based on the above ZIP header + // offsets: + $first_file_start = array_sum(unpack('v2', substr($gzData, 26, 4))); + if (false !== ($decompressed = @gzinflate(substr($gzData, 30 + $first_file_start)))) { + return $decompressed; + } + return false; + } + + // Finally fall back to straight gzinflate + if (false !== ($decompressed = @gzinflate($gzData))) { + return $decompressed; + } + + // Fallback for all above failing, not expected, but included for + // debugging and preventing regressions and to track stats + if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { + return $decompressed; + } + + return false; + } + + public static function match_domain($host, $reference) { + // Check for a direct match + if ($host === $reference) { + return true; + } + + // Calculate the valid wildcard match if the host is not an IP address + // Also validates that the host has 3 parts or more, as per Firefox's + // ruleset. + $parts = explode('.', $host); + if (ip2long($host) === false && count($parts) >= 3) { + $parts[0] = '*'; + $wildcard = implode('.', $parts); + if ($wildcard === $reference) { + return true; + } + } + + return false; + } +}