From 0e128f28067639a93d62899763008a7ffd90bfc5 Mon Sep 17 00:00:00 2001 From: ozh Date: Sat, 28 Nov 2015 18:23:11 +0100 Subject: [PATCH] Sync Requests Latest OK build on master: https://github.com/rmccue/Requests/tree/6f69bd206bfa4f00a1ec355ab7226ee64ac988dd --- includes/Requests/Requests.php | 134 +- includes/Requests/Requests/Auth/Basic.php | 2 +- includes/Requests/Requests/Cookie.php | 201 +- includes/Requests/Requests/Cookie/Jar.php | 32 +- includes/Requests/Requests/Exception/HTTP.php | 8 +- .../Requests/Exception/HTTP/Unknown.php | 4 +- includes/Requests/Requests/Hooks.php | 7 + includes/Requests/Requests/IDNAEncoder.php | 4 +- includes/Requests/Requests/IPv6.php | 355 ++- includes/Requests/Requests/IRI.php | 2210 ++++++++--------- includes/Requests/Requests/Proxy/HTTP.php | 17 +- includes/Requests/Requests/Response.php | 34 +- .../Requests/Requests/Response/Headers.php | 9 +- includes/Requests/Requests/SSL.php | 5 +- includes/Requests/Requests/Session.php | 6 +- includes/Requests/Requests/Transport/cURL.php | 169 +- .../Requests/Requests/Transport/fsockopen.php | 151 +- .../Utility/CaseInsensitiveDictionary.php | 14 +- .../Requests/Utility/FilteredIterator.php | 7 + 19 files changed, 1760 insertions(+), 1609 deletions(-) diff --git a/includes/Requests/Requests.php b/includes/Requests/Requests.php index 065227e..8f1f7be 100644 --- a/includes/Requests/Requests.php +++ b/includes/Requests/Requests.php @@ -54,6 +54,20 @@ class Requests { */ const DELETE = 'DELETE'; + /** + * OPTIONS method + * + * @var string + */ + const OPTIONS = 'OPTIONS'; + + /** + * TRACE method + * + * @var string + */ + const TRACE = 'TRACE'; + /** * PATCH method * @@ -176,8 +190,9 @@ protected static function get_transport($capabilities = array()) { // Find us a working transport foreach (self::$transports as $class) { - if (!class_exists($class)) + if (!class_exists($class)) { continue; + } $result = call_user_func(array($class, 'test'), $capabilities); if ($result) { @@ -188,7 +203,7 @@ protected static function get_transport($capabilities = array()) { if (self::$transport[$cap_string] === null) { throw new Requests_Exception('No working transports found', 'notransport', self::$transports); } - + return new self::$transport[$cap_string](); } @@ -219,6 +234,13 @@ public static function head($url, $headers = array(), $options = array()) { public static function delete($url, $headers = array(), $options = array()) { return self::request($url, $headers, null, self::DELETE, $options); } + + /** + * Send a TRACE request + */ + public static function trace($url, $headers = array(), $options = array()) { + return self::request($url, $headers, null, self::TRACE, $options); + } /**#@-*/ /**#@+ @@ -242,6 +264,13 @@ public static function put($url, $headers = array(), $data = array(), $options = return self::request($url, $headers, $data, self::PUT, $options); } + /** + * Send an OPTIONS request + */ + public static function options($url, $headers = array(), $data = array(), $options = array()) { + return self::request($url, $headers, $data, self::OPTIONS, $options); + } + /** * Send a PATCH request * @@ -300,12 +329,15 @@ public static function patch($url, $headers, $data = array(), $options = array() * (string|boolean, default: library/Requests/Transport/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) * * @throws Requests_Exception On invalid URLs (`nonhttp`) * * @param string $url URL to request * @param array $headers Extra headers to send with the request - * @param array $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @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 Requests_Response @@ -326,7 +358,8 @@ public static function request($url, $headers = array(), $data = array(), $type if (is_string($options['transport'])) { $transport = new $transport(); } - } else { + } + else { $need_ssl = (0 === stripos($url, 'https://')); $capabilities = array('ssl' => $need_ssl); $transport = self::get_transport($capabilities); @@ -459,6 +492,7 @@ protected static function get_default_options($multirequest = false) { 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, + 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, @@ -472,7 +506,7 @@ protected static function get_default_options($multirequest = false) { 'idn' => true, 'hooks' => null, 'transport' => null, - 'verify' => dirname( __FILE__ ) . '/Requests/Transport/cacert.pem', + 'verify' => dirname(__FILE__) . '/Requests/Transport/cacert.pem', 'verifyname' => true, ); if ($multirequest !== false) { @@ -486,7 +520,7 @@ protected static function get_default_options($multirequest = false) { * * @param string $url URL to request * @param array $headers Extra headers to send with the request - * @param array $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @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 @@ -529,6 +563,15 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio $iri->host = Requests_IDNAEncoder::encode($iri->ihost); $url = $iri->uri; } + + 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'; + } + } } /** @@ -571,11 +614,12 @@ protected static function parse_response($headers, $url, $req_headers, $req_data // 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); + 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->status_code = (int) $matches[1]; + $return->protocol_version = (float) $matches[1]; + $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } @@ -601,19 +645,19 @@ protected static function parse_response($headers, $url, $req_headers, $req_data $options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options)); - if ((in_array($return->status_code, array(300, 301, 302, 303, 307)) || $return->status_code > 307 && $return->status_code < 400) && $options['follow_redirects'] === true) { + if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { - $options['type'] = Requests::GET; + $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; - if (strpos ($location, 'http://') !== 0 && strpos ($location, 'https://') !== 0) { + 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; } - $redirected = self::request($location, $req_headers, $req_data, false, $options); + $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } @@ -634,13 +678,17 @@ protected static function parse_response($headers, $url, $req_headers, $req_data * Internal use only. Converts a raw HTTP response to a Requests_Response * while still executing a multiple request. * - * @param string $headers Full response text including headers and body + * @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 { - $response = self::parse_response($response, $request['url'], $request['headers'], $request['data'], $request['options']); + $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; @@ -663,7 +711,7 @@ protected static function decode_chunked($data) { $encoded = $data; while (true) { - $is_chunked = (bool) preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches ); + $is_chunked = (bool) preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; @@ -676,7 +724,7 @@ protected static function decode_chunked($data) { } $chunk_length = strlen($matches[0]); - $decoded .= $part = substr($encoded, $chunk_length, $length); + $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { @@ -698,7 +746,7 @@ protected static function decode_chunked($data) { public static function flatten($array) { $return = array(); foreach ($array as $key => $value) { - $return[] = "$key: $value"; + $return[] = sprintf('%s: %s', $key, $value); } return $return; } @@ -720,7 +768,6 @@ public static function flattern($array) { * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * - * @todo Make this smarter by defaulting to whatever the headers say first * @param string $data Compressed data in one of the above formats * @return string Decompressed string */ @@ -769,23 +816,26 @@ public static function decompress($data) { 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" ) { + 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) ); + $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 ) + if ($flg & 8) { $i = strpos($gzData, "\0", $i) + 1; - if ( $flg & 16 ) + } + if ($flg & 16) { $i = strpos($gzData, "\0", $i) + 1; - if ( $flg & 2 ) + } + if ($flg & 2) { $i = $i + 2; + } } - $decompressed = self::compatible_gzinflate( substr( $gzData, $i ) ); - if ( false !== $decompressed ) { + $decompressed = self::compatible_gzinflate(substr($gzData, $i)); + if (false !== $decompressed) { return $decompressed; } } @@ -801,55 +851,57 @@ public static function compatible_gzinflate($gzData) { $huffman_encoded = false; // low nibble of first byte should be 0x08 - list( , $first_nibble ) = unpack( 'h', $gzData ); + list(, $first_nibble) = unpack('h', $gzData); // First 2 bytes should be divisible by 0x1F - list( , $first_two_bytes ) = unpack( 'n', $gzData ); + list(, $first_two_bytes) = unpack('n', $gzData); - if ( 0x08 == $first_nibble && 0 == ( $first_two_bytes % 0x1F ) ) + if (0x08 == $first_nibble && 0 == ($first_two_bytes % 0x1F)) { $huffman_encoded = true; + } - if ( $huffman_encoded ) { - if ( false !== ( $decompressed = @gzinflate( substr( $gzData, 2 ) ) ) ) + if ($huffman_encoded) { + if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { return $decompressed; + } } - if ( "\x50\x4b\x03\x04" == substr( $gzData, 0, 4 ) ) { + 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 ) ); + 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 ) ); + $zip_compressed_on_the_fly = (0x08 == (0x08 & $general_purpose_flag)); - if ( ! $zip_compressed_on_the_fly ) { + 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 ) ) ) ) { + $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 ) ) ) { + 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 ) ) ) ) { + if (false !== ($decompressed = @gzinflate(substr($gzData, 2)))) { return $decompressed; } diff --git a/includes/Requests/Requests/Auth/Basic.php b/includes/Requests/Requests/Auth/Basic.php index b5f420e..a355cfd 100644 --- a/includes/Requests/Requests/Auth/Basic.php +++ b/includes/Requests/Requests/Auth/Basic.php @@ -74,7 +74,7 @@ public function curl_before_send(&$handle) { * @param string $out HTTP header string */ public function fsockopen_header(&$out) { - $out .= "Authorization: Basic " . base64_encode($this->getAuthString()) . "\r\n"; + $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); } /** diff --git a/includes/Requests/Requests/Cookie.php b/includes/Requests/Requests/Cookie.php index d978817..c239fed 100644 --- a/includes/Requests/Requests/Cookie.php +++ b/includes/Requests/Requests/Cookie.php @@ -14,23 +14,26 @@ */ class Requests_Cookie { /** - * + * Cookie name. + * * @var string */ public $name; /** + * Cookie value. + * * @var string */ public $value; /** * Cookie attributes - * + * * Valid keys are (currently) path, domain, expires, max-age, secure and * httponly. * - * @var array + * @var Requests_Utility_CaseInsensitiveDictionary|array Array-like object */ public $attributes = array(); @@ -44,14 +47,24 @@ class Requests_Cookie { */ public $flags = array(); + /** + * Reference time for relative calculations + * + * This is used in place of `time()` when calculating Max-Age expiration and + * checking time validity. + * + * @var int + */ + public $reference_time = 0; + /** * Create a new cookie object * * @param string $name * @param string $value - * @param array $attributes Associative array of attribute data + * @param array|Requests_Utility_CaseInsensitiveDictionary $attributes Associative array of attribute data */ - public function __construct($name, $value, $attributes = array(), $flags = array()) { + public function __construct($name, $value, $attributes = array(), $flags = array(), $reference_time = null) { $this->name = $name; $this->value = $value; $this->attributes = $attributes; @@ -63,29 +76,56 @@ public function __construct($name, $value, $attributes = array(), $flags = array ); $this->flags = array_merge($default_flags, $flags); + $this->reference_time = time(); + if ($reference_time !== null) { + $this->reference_time = $reference_time; + } + $this->normalize(); } + /** + * Check if a cookie is expired. + * + * Checks the age against $this->reference_time to determine if the cookie + * is expired. + * + * @return boolean True if expired, false if time is valid. + */ + public function is_expired() { + // RFC6265, s. 4.1.2.2: + // If a cookie has both the Max-Age and the Expires attribute, the Max- + // Age attribute has precedence and controls the expiration date of the + // cookie. + if (isset($this->attributes['max-age'])) { + $max_age = $this->attributes['max-age']; + return $max_age < $this->reference_time; + } + + if (isset($this->attributes['expires'])) { + $expires = $this->attributes['expires']; + return $expires < $this->reference_time; + } + + return false; + } + /** * Check if a cookie is valid for a given URI * * @param Requests_IRI $uri URI to check * @return boolean Whether the cookie is valid for the given URI */ - public function uriMatches(Requests_IRI $uri) { - if (!$this->domainMatches($uri->host)) { + public function uri_matches(Requests_IRI $uri) { + if (!$this->domain_matches($uri->host)) { return false; } - if (!$this->pathMatches($uri->path)) { + if (!$this->path_matches($uri->path)) { return false; } - if (!empty($this->attributes['secure']) && $uri->scheme !== 'https') { - return false; - } - - return true; + return empty($this->attributes['secure']) || $uri->scheme === 'https'; } /** @@ -94,7 +134,7 @@ public function uriMatches(Requests_IRI $uri) { * @param string $string Domain to check * @return boolean Whether the cookie is valid for the given domain */ - public function domainMatches($string) { + public function domain_matches($string) { if (!isset($this->attributes['domain'])) { // Cookies created manually; cookies created by Requests will set // the domain to the requested domain @@ -131,12 +171,8 @@ public function domainMatches($string) { return false; } - if (preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string)) { - // The string should be a host name (i.e., not an IP address). - return false; - } - - return true; + // The string should be a host name (i.e., not an IP address). + return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string); } /** @@ -147,7 +183,7 @@ public function domainMatches($string) { * @param string $request_path Path to check * @return boolean Whether the cookie is valid for the given path */ - public function pathMatches($request_path) { + public function path_matches($request_path) { if (empty($request_path)) { // Normalize empty path to root $request_path = '/'; @@ -192,13 +228,10 @@ public function pathMatches($request_path) { public function normalize() { foreach ($this->attributes as $key => $value) { $orig_value = $value; - switch ($key) { - case 'domain': - // Domain normalization, as per RFC 6265 section 5.2.3 - if ($value[0] === '.') { - $value = substr($value, 1); - } - break; + $value = $this->normalize_attribute($key, $value); + if ($value === null) { + unset($this->attributes[$key]); + continue; } if ($value !== $orig_value) { @@ -209,6 +242,64 @@ public function normalize() { return true; } + /** + * Parse an individual cookie attribute + * + * Handles parsing individual attributes from the cookie values. + * + * @param string $name Attribute name + * @param string|boolean $value Attribute value (string value, or true if empty/flag) + * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped) + */ + protected function normalize_attribute($name, $value) { + switch (strtolower($name)) { + case 'expires': + // Expiration parsing, as per RFC 6265 section 5.2.1 + if (is_int($value)) { + return $value; + } + + $expiry_time = strtotime($value); + if ($expiry_time === false) { + return null; + } + + return $expiry_time; + + case 'max-age': + // Expiration parsing, as per RFC 6265 section 5.2.2 + if (is_int($value)) { + return $value; + } + + // Check that we have a valid age + if (!preg_match('/^-?\d+$/', $value)) { + return null; + } + + $delta_seconds = (int) $value; + if ($delta_seconds <= 0) { + $expiry_time = 0; + } + else { + $expiry_time = $this->reference_time + $delta_seconds; + } + + return $expiry_time; + + case 'domain': + // Domain normalization, as per RFC 6265 section 5.2.3 + if ($value[0] === '.') { + $value = substr($value, 1); + } + + return $value; + + default: + return $value; + } + } + /** * Format a cookie for a Cookie header * @@ -216,10 +307,20 @@ public function normalize() { * * @return string Cookie formatted for Cookie header */ - public function formatForHeader() { + public function format_for_header() { return sprintf('%s=%s', $this->name, $this->value); } + /** + * Format a cookie for a Cookie header + * + * @deprecated Use {@see Requests_Cookie::format_for_header} + * @return string + */ + public function formatForHeader() { + return $this->format_for_header(); + } + /** * Format a cookie for a Set-Cookie header * @@ -228,8 +329,8 @@ public function formatForHeader() { * * @return string Cookie formatted for Set-Cookie header */ - public function formatForSetCookie() { - $header_value = $this->formatForHeader(); + public function format_for_set_cookie() { + $header_value = $this->format_for_header(); if (!empty($this->attributes)) { $parts = array(); foreach ($this->attributes as $key => $value) { @@ -247,6 +348,16 @@ public function formatForSetCookie() { return $header_value; } + /** + * Format a cookie for a Set-Cookie header + * + * @deprecated Use {@see Requests_Cookie::format_for_set_cookie} + * @return string + */ + public function formatForSetCookie() { + return $this->format_for_set_cookie(); + } + /** * Get the cookie value * @@ -266,7 +377,7 @@ public function __toString() { * @param string Cookie header value (from a Set-Cookie header) * @return Requests_Cookie Parsed cookie object */ - public static function parse($string, $name = '') { + public static function parse($string, $name = '', $reference_time = null) { $parts = explode(';', $string); $kvparts = array_shift($parts); @@ -307,16 +418,18 @@ public static function parse($string, $name = '') { } } - return new Requests_Cookie($name, $value, $attributes); + return new Requests_Cookie($name, $value, $attributes, array(), $reference_time); } /** * Parse all Set-Cookie headers from request headers * - * @param Requests_Response_Headers $headers + * @param Requests_Response_Headers $headers Headers to parse from + * @param Requests_IRI|null $origin URI for comparing cookie origins + * @param int|null $time Reference time for expiration calculation * @return array */ - public static function parseFromHeaders(Requests_Response_Headers $headers, Requests_IRI $origin = null) { + public static function parse_from_headers(Requests_Response_Headers $headers, Requests_IRI $origin = null, $time = null) { $cookie_headers = $headers->getValues('Set-Cookie'); if (empty($cookie_headers)) { return array(); @@ -324,15 +437,15 @@ public static function parseFromHeaders(Requests_Response_Headers $headers, Requ $cookies = array(); foreach ($cookie_headers as $header) { - $parsed = self::parse($header); + $parsed = self::parse($header, '', $time); // Default domain/path attributes if (empty($parsed->attributes['domain']) && !empty($origin)) { $parsed->attributes['domain'] = $origin->host; - $parsed->flags['host-only'] = false; + $parsed->flags['host-only'] = true; } else { - $parsed->flags['host-only'] = true; + $parsed->flags['host-only'] = false; } $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/'); @@ -362,7 +475,7 @@ public static function parseFromHeaders(Requests_Response_Headers $headers, Requ } // Reject invalid cookie domains - if (!$parsed->domainMatches($origin->host)) { + if (!empty($origin) && !$parsed->domain_matches($origin->host)) { continue; } @@ -371,4 +484,14 @@ public static function parseFromHeaders(Requests_Response_Headers $headers, Requ return $cookies; } + + /** + * Parse all Set-Cookie headers from request headers + * + * @deprecated Use {@see Requests_Cookie::parse_from_headers} + * @return string + */ + public static function parseFromHeaders(Requests_Response_Headers $headers) { + return self::parse_from_headers($headers); + } } diff --git a/includes/Requests/Requests/Cookie/Jar.php b/includes/Requests/Requests/Cookie/Jar.php index 82c332d..60f01bd 100644 --- a/includes/Requests/Requests/Cookie/Jar.php +++ b/includes/Requests/Requests/Cookie/Jar.php @@ -35,7 +35,7 @@ public function __construct($cookies = array()) { * @param string|Requests_Cookie $cookie * @return Requests_Cookie */ - public function normalizeCookie($cookie, $key = null) { + public function normalize_cookie($cookie, $key = null) { if ($cookie instanceof Requests_Cookie) { return $cookie; } @@ -43,6 +43,16 @@ public function normalizeCookie($cookie, $key = null) { return Requests_Cookie::parse($cookie, $key); } + /** + * Normalise cookie data into a Requests_Cookie + * + * @deprecated Use {@see Requests_Cookie_Jar::normalize_cookie} + * @return Requests_Cookie + */ + public function normalizeCookie($cookie, $key = null) { + return $this->normalize_cookie($cookie, $key); + } + /** * Check if the given item exists * @@ -60,8 +70,9 @@ public function offsetExists($key) { * @return string Item value */ public function offsetGet($key) { - if (!isset($this->cookies[$key])) + if (!isset($this->cookies[$key])) { return null; + } return $this->cookies[$key]; } @@ -122,17 +133,22 @@ public function register(Requests_Hooker $hooks) { * @param array $options */ public function before_request($url, &$headers, &$data, &$type, &$options) { - if ( ! $url instanceof Requests_IRI ) { + if (!$url instanceof Requests_IRI) { $url = new Requests_IRI($url); } if (!empty($this->cookies)) { $cookies = array(); foreach ($this->cookies as $key => $cookie) { - $cookie = $this->normalizeCookie($cookie, $key); + $cookie = $this->normalize_cookie($cookie, $key); + + // Skip expired cookies + if ($cookie->is_expired()) { + continue; + } - if ( $cookie->domainMatches( $url->host ) ) { - $cookies[] = $cookie->formatForHeader(); + if ($cookie->domain_matches($url->host)) { + $cookies[] = $cookie->format_for_header(); } } @@ -147,11 +163,11 @@ public function before_request($url, &$headers, &$data, &$type, &$options) { */ public function before_redirect_check(Requests_Response &$return) { $url = $return->url; - if ( ! $url instanceof Requests_IRI ) { + if (!$url instanceof Requests_IRI) { $url = new Requests_IRI($url); } - $cookies = Requests_Cookie::parseFromHeaders($return->headers, $url); + $cookies = Requests_Cookie::parse_from_headers($return->headers, $url); $this->cookies = array_merge($this->cookies, $cookies); $return->cookies = $this; } diff --git a/includes/Requests/Requests/Exception/HTTP.php b/includes/Requests/Requests/Exception/HTTP.php index dc8954f..9ac6a87 100644 --- a/includes/Requests/Requests/Exception/HTTP.php +++ b/includes/Requests/Requests/Exception/HTTP.php @@ -31,7 +31,7 @@ class Requests_Exception_HTTP extends Requests_Exception { * There is no mechanism to pass in the status code, as this is set by the * subclass used. Reason phrases can vary, however. * - * @param string $reason Reason phrase + * @param string|null $reason Reason phrase * @param mixed $data Associated data */ public function __construct($reason = null, $data = null) { @@ -53,10 +53,14 @@ public function getReason() { /** * Get the correct exception class for a given error code * - * @param int $code HTTP status code + * @param int|bool $code HTTP status code, or false if unavailable * @return string Exception class name to use */ public static function get_class($code) { + if (!$code) { + return 'Requests_Exception_HTTP_Unknown'; + } + $class = sprintf('Requests_Exception_HTTP_%d', $code); if (class_exists($class)) { return $class; diff --git a/includes/Requests/Requests/Exception/HTTP/Unknown.php b/includes/Requests/Requests/Exception/HTTP/Unknown.php index 2c4fc7e..c70f589 100644 --- a/includes/Requests/Requests/Exception/HTTP/Unknown.php +++ b/includes/Requests/Requests/Exception/HTTP/Unknown.php @@ -14,7 +14,7 @@ class Requests_Exception_HTTP_Unknown extends Requests_Exception_HTTP { /** * HTTP status code * - * @var integer + * @var integer|bool Code if available, false if an error occurred */ protected $code = 0; @@ -31,7 +31,7 @@ class Requests_Exception_HTTP_Unknown extends Requests_Exception_HTTP { * If `$data` is an instance of {@see Requests_Response}, uses the status * code from it. Otherwise, sets as 0 * - * @param string $reason Reason phrase + * @param string|null $reason Reason phrase * @param mixed $data Associated data */ public function __construct($reason = null, $data = null) { diff --git a/includes/Requests/Requests/Hooks.php b/includes/Requests/Requests/Hooks.php index 7a99b9b..2e61c73 100644 --- a/includes/Requests/Requests/Hooks.php +++ b/includes/Requests/Requests/Hooks.php @@ -13,6 +13,13 @@ * @subpackage Utilities */ class Requests_Hooks implements Requests_Hooker { + /** + * Registered callbacks for each hook + * + * @var array + */ + protected $hooks = array(); + /** * Constructor */ diff --git a/includes/Requests/Requests/IDNAEncoder.php b/includes/Requests/Requests/IDNAEncoder.php index 53cdd0a..eaecb73 100644 --- a/includes/Requests/Requests/IDNAEncoder.php +++ b/includes/Requests/Requests/IDNAEncoder.php @@ -237,6 +237,7 @@ public static function punycode_encode($input) { $h = $b = 0; // see loop # copy them to the output in order $codepoints = self::utf8_to_codepoints($input); + $extended = array(); foreach ($codepoints as $char) { if ($char < 128) { @@ -303,7 +304,6 @@ public static function punycode_encode($input) { } # output the code point for digit t + ((q - t) mod (base - t)) $digit = $t + (($q - $t) % (self::BOOTSTRAP_BASE - $t)); - //printf('needed delta is %d, encodes as "%s"' . PHP_EOL, $delta, self::digit_to_char($digit)); $output .= self::digit_to_char($digit); # let q = (q - t) div (base - t) $q = floor(($q - $t) / (self::BOOTSTRAP_BASE - $t)); @@ -311,10 +311,8 @@ public static function punycode_encode($input) { } # output the code point for digit q $output .= self::digit_to_char($q); - //printf('needed delta is %d, encodes as "%s"' . PHP_EOL, $delta, self::digit_to_char($q)); # let bias = adapt(delta, h + 1, test h equals b?) $bias = self::adapt($delta, $h + 1, $h === $b); - //printf('bias becomes %d' . PHP_EOL, $bias); # let delta = 0 $delta = 0; # increment h diff --git a/includes/Requests/Requests/IPv6.php b/includes/Requests/Requests/IPv6.php index 5e5c259..204dbd7 100644 --- a/includes/Requests/Requests/IPv6.php +++ b/includes/Requests/Requests/IPv6.php @@ -15,207 +15,176 @@ * @package Requests * @subpackage Utilities */ -class Requests_IPv6 -{ - /** - * Uncompresses an IPv6 address - * - * RFC 4291 allows you to compress consecutive zero pieces in an address to - * '::'. This method expects a valid IPv6 address and expands the '::' to - * the required number of zero pieces. - * - * Example: FF01::101 -> FF01:0:0:0:0:0:0:101 - * ::1 -> 0:0:0:0:0:0:0:1 - * - * @author Alexander Merz - * @author elfrink at introweb dot nl - * @author Josh Peck - * @copyright 2003-2005 The PHP Group - * @license http://www.opensource.org/licenses/bsd-license.php - * @param string $ip An IPv6 address - * @return string The uncompressed IPv6 address - */ - public static function uncompress($ip) - { - $c1 = -1; - $c2 = -1; - if (substr_count($ip, '::') === 1) - { - list($ip1, $ip2) = explode('::', $ip); - if ($ip1 === '') - { - $c1 = -1; - } - else - { - $c1 = substr_count($ip1, ':'); - } - if ($ip2 === '') - { - $c2 = -1; - } - else - { - $c2 = substr_count($ip2, ':'); - } - if (strpos($ip2, '.') !== false) - { - $c2++; - } - // :: - if ($c1 === -1 && $c2 === -1) - { - $ip = '0:0:0:0:0:0:0:0'; - } - // ::xxx - else if ($c1 === -1) - { - $fill = str_repeat('0:', 7 - $c2); - $ip = str_replace('::', $fill, $ip); - } - // xxx:: - else if ($c2 === -1) - { - $fill = str_repeat(':0', 7 - $c1); - $ip = str_replace('::', $fill, $ip); - } - // xxx::xxx - else - { - $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); - $ip = str_replace('::', $fill, $ip); - } - } - return $ip; - } +class Requests_IPv6 { + /** + * Uncompresses an IPv6 address + * + * RFC 4291 allows you to compress consecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and expands the '::' to + * the required number of zero pieces. + * + * Example: FF01::101 -> FF01:0:0:0:0:0:0:101 + * ::1 -> 0:0:0:0:0:0:0:1 + * + * @author Alexander Merz + * @author elfrink at introweb dot nl + * @author Josh Peck + * @copyright 2003-2005 The PHP Group + * @license http://www.opensource.org/licenses/bsd-license.php + * @param string $ip An IPv6 address + * @return string The uncompressed IPv6 address + */ + public static function uncompress($ip) { + if (substr_count($ip, '::') !== 1) { + return $ip; + } - /** - * Compresses an IPv6 address - * - * RFC 4291 allows you to compress consecutive zero pieces in an address to - * '::'. This method expects a valid IPv6 address and compresses consecutive - * zero pieces to '::'. - * - * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 - * 0:0:0:0:0:0:0:1 -> ::1 - * - * @see uncompress() - * @param string $ip An IPv6 address - * @return string The compressed IPv6 address - */ - public static function compress($ip) - { - // Prepare the IP to be compressed - $ip = self::uncompress($ip); - $ip_parts = self::split_v6_v4($ip); + list($ip1, $ip2) = explode('::', $ip); + $c1 = ($ip1 === '') ? -1 : substr_count($ip1, ':'); + $c2 = ($ip2 === '') ? -1 : substr_count($ip2, ':'); - // Replace all leading zeros - $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); + if (strpos($ip2, '.') !== false) { + $c2++; + } + // :: + if ($c1 === -1 && $c2 === -1) { + $ip = '0:0:0:0:0:0:0:0'; + } + // ::xxx + else if ($c1 === -1) { + $fill = str_repeat('0:', 7 - $c2); + $ip = str_replace('::', $fill, $ip); + } + // xxx:: + else if ($c2 === -1) { + $fill = str_repeat(':0', 7 - $c1); + $ip = str_replace('::', $fill, $ip); + } + // xxx::xxx + else { + $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); + $ip = str_replace('::', $fill, $ip); + } + return $ip; + } - // Find bunches of zeros - if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) - { - $max = 0; - $pos = null; - foreach ($matches[0] as $match) - { - if (strlen($match[0]) > $max) - { - $max = strlen($match[0]); - $pos = $match[1]; - } - } + /** + * Compresses an IPv6 address + * + * RFC 4291 allows you to compress consecutive zero pieces in an address to + * '::'. This method expects a valid IPv6 address and compresses consecutive + * zero pieces to '::'. + * + * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 + * 0:0:0:0:0:0:0:1 -> ::1 + * + * @see uncompress() + * @param string $ip An IPv6 address + * @return string The compressed IPv6 address + */ + public static function compress($ip) { + // Prepare the IP to be compressed + $ip = self::uncompress($ip); + $ip_parts = self::split_v6_v4($ip); - $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); - } + // Replace all leading zeros + $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); - if ($ip_parts[1] !== '') - { - return implode(':', $ip_parts); - } - else - { - return $ip_parts[0]; - } - } + // Find bunches of zeros + if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) { + $max = 0; + $pos = null; + foreach ($matches[0] as $match) { + if (strlen($match[0]) > $max) { + $max = strlen($match[0]); + $pos = $match[1]; + } + } - /** - * Splits an IPv6 address into the IPv6 and IPv4 representation parts - * - * RFC 4291 allows you to represent the last two parts of an IPv6 address - * using the standard IPv4 representation - * - * Example: 0:0:0:0:0:0:13.1.68.3 - * 0:0:0:0:0:FFFF:129.144.52.38 - * - * @param string $ip An IPv6 address - * @return array [0] contains the IPv6 represented part, and [1] the IPv4 represented part - */ - private static function split_v6_v4($ip) - { - if (strpos($ip, '.') !== false) - { - $pos = strrpos($ip, ':'); - $ipv6_part = substr($ip, 0, $pos); - $ipv4_part = substr($ip, $pos + 1); - return array($ipv6_part, $ipv4_part); - } - else - { - return array($ip, ''); - } - } + $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); + } - /** - * Checks an IPv6 address - * - * Checks if the given IP is a valid IPv6 address - * - * @param string $ip An IPv6 address - * @return bool true if $ip is a valid IPv6 address - */ - public static function check_ipv6($ip) - { - $ip = self::uncompress($ip); - list($ipv6, $ipv4) = self::split_v6_v4($ip); - $ipv6 = explode(':', $ipv6); - $ipv4 = explode('.', $ipv4); - if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) - { - foreach ($ipv6 as $ipv6_part) - { - // The section can't be empty - if ($ipv6_part === '') - return false; + if ($ip_parts[1] !== '') { + return implode(':', $ip_parts); + } + else { + return $ip_parts[0]; + } + } - // Nor can it be over four characters - if (strlen($ipv6_part) > 4) - return false; + /** + * Splits an IPv6 address into the IPv6 and IPv4 representation parts + * + * RFC 4291 allows you to represent the last two parts of an IPv6 address + * using the standard IPv4 representation + * + * Example: 0:0:0:0:0:0:13.1.68.3 + * 0:0:0:0:0:FFFF:129.144.52.38 + * + * @param string $ip An IPv6 address + * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part + */ + protected static function split_v6_v4($ip) { + if (strpos($ip, '.') !== false) { + $pos = strrpos($ip, ':'); + $ipv6_part = substr($ip, 0, $pos); + $ipv4_part = substr($ip, $pos + 1); + return array($ipv6_part, $ipv4_part); + } + else { + return array($ip, ''); + } + } - // Remove leading zeros (this is safe because of the above) - $ipv6_part = ltrim($ipv6_part, '0'); - if ($ipv6_part === '') - $ipv6_part = '0'; + /** + * Checks an IPv6 address + * + * Checks if the given IP is a valid IPv6 address + * + * @param string $ip An IPv6 address + * @return bool true if $ip is a valid IPv6 address + */ + public static function check_ipv6($ip) { + $ip = self::uncompress($ip); + list($ipv6, $ipv4) = self::split_v6_v4($ip); + $ipv6 = explode(':', $ipv6); + $ipv4 = explode('.', $ipv4); + if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) { + foreach ($ipv6 as $ipv6_part) { + // The section can't be empty + if ($ipv6_part === '') { + return false; + } - // Check the value is valid - $value = hexdec($ipv6_part); - if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) - return false; - } - if (count($ipv4) === 4) - { - foreach ($ipv4 as $ipv4_part) - { - $value = (int) $ipv4_part; - if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) - return false; - } - } - return true; - } - else - { - return false; - } - } + // Nor can it be over four characters + if (strlen($ipv6_part) > 4) { + return false; + } + + // Remove leading zeros (this is safe because of the above) + $ipv6_part = ltrim($ipv6_part, '0'); + if ($ipv6_part === '') { + $ipv6_part = '0'; + } + + // Check the value is valid + $value = hexdec($ipv6_part); + if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) { + return false; + } + } + if (count($ipv4) === 4) { + foreach ($ipv4 as $ipv4_part) { + $value = (int) $ipv4_part; + if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) { + return false; + } + } + } + return true; + } + else { + return false; + } + } } diff --git a/includes/Requests/Requests/IRI.php b/includes/Requests/Requests/IRI.php index 26f215b..44a9517 100644 --- a/includes/Requests/Requests/IRI.php +++ b/includes/Requests/Requests/IRI.php @@ -45,1177 +45,1043 @@ * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo * @license http://www.opensource.org/licenses/bsd-license.php * @link http://hg.gsnedders.com/iri/ + * + * @property string $iri IRI we're working with + * @property-read string $uri IRI in URI form, {@see to_uri} + * @property string $scheme Scheme part of the IRI + * @property string $authority Authority part, formatted for a URI (userinfo + host + port) + * @property string $iauthority Authority part of the IRI (userinfo + host + port) + * @property string $userinfo Userinfo part, formatted for a URI (after '://' and before '@') + * @property string $iuserinfo Userinfo part of the IRI (after '://' and before '@') + * @property string $host Host part, formatted for a URI + * @property string $ihost Host part of the IRI + * @property string $port Port part of the IRI (after ':') + * @property string $path Path part, formatted for a URI (after first '/') + * @property string $ipath Path part of the IRI (after first '/') + * @property string $query Query part, formatted for a URI (after '?') + * @property string $iquery Query part of the IRI (after '?') + * @property string $fragment Fragment, formatted for a URI (after '#') + * @property string $ifragment Fragment part of the IRI (after '#') */ -class Requests_IRI -{ - /** - * Scheme - * - * @var string - */ - protected $scheme = null; - - /** - * User Information - * - * @var string - */ - protected $iuserinfo = null; - - /** - * ihost - * - * @var string - */ - protected $ihost = null; - - /** - * Port - * - * @var string - */ - protected $port = null; - - /** - * ipath - * - * @var string - */ - protected $ipath = ''; - - /** - * iquery - * - * @var string - */ - protected $iquery = null; - - /** - * ifragment - * - * @var string - */ - protected $ifragment = null; - - /** - * Normalization database - * - * Each key is the scheme, each value is an array with each key as the IRI - * part and value as the default value for that part. - */ - protected $normalization = array( - 'acap' => array( - 'port' => 674 - ), - 'dict' => array( - 'port' => 2628 - ), - 'file' => array( - 'ihost' => 'localhost' - ), - 'http' => array( - 'port' => 80, - ), - 'https' => array( - 'port' => 443, - ), - ); - - /** - * Return the entire IRI when you try and read the object as a string - * - * @return string - */ - public function __toString() - { - return $this->get_iri(); - } - - /** - * Overload __set() to provide access via properties - * - * @param string $name Property name - * @param mixed $value Property value - */ - public function __set($name, $value) - { - if (method_exists($this, 'set_' . $name)) - { - call_user_func(array($this, 'set_' . $name), $value); - } - elseif ( - $name === 'iauthority' - || $name === 'iuserinfo' - || $name === 'ihost' - || $name === 'ipath' - || $name === 'iquery' - || $name === 'ifragment' - ) - { - call_user_func(array($this, 'set_' . substr($name, 1)), $value); - } - } - - /** - * Overload __get() to provide access via properties - * - * @param string $name Property name - * @return mixed - */ - public function __get($name) - { - // isset() returns false for null, we don't want to do that - // Also why we use array_key_exists below instead of isset() - $props = get_object_vars($this); - - if ( - $name === 'iri' || - $name === 'uri' || - $name === 'iauthority' || - $name === 'authority' - ) - { - $return = $this->{"get_$name"}(); - } - elseif (array_key_exists($name, $props)) - { - $return = $this->$name; - } - // host -> ihost - elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) - { - $name = $prop; - $return = $this->$prop; - } - // ischeme -> scheme - elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) - { - $name = $prop; - $return = $this->$prop; - } - else - { - trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); - $return = null; - } - - if ($return === null && isset($this->normalization[$this->scheme][$name])) - { - return $this->normalization[$this->scheme][$name]; - } - else - { - return $return; - } - } - - /** - * Overload __isset() to provide access via properties - * - * @param string $name Property name - * @return bool - */ - public function __isset($name) - { - if (method_exists($this, 'get_' . $name) || isset($this->$name)) - { - return true; - } - else - { - return false; - } - } - - /** - * Overload __unset() to provide access via properties - * - * @param string $name Property name - */ - public function __unset($name) - { - if (method_exists($this, 'set_' . $name)) - { - call_user_func(array($this, 'set_' . $name), ''); - } - } - - /** - * Create a new IRI object, from a specified string - * - * @param string $iri - */ - public function __construct($iri = null) - { - $this->set_iri($iri); - } - - /** - * Create a new IRI object by resolving a relative IRI - * - * Returns false if $base is not absolute, otherwise an IRI. - * - * @param IRI|string $base (Absolute) Base IRI - * @param IRI|string $relative Relative IRI - * @return IRI|false - */ - public static function absolutize($base, $relative) - { - if (!($relative instanceof Requests_IRI)) - { - $relative = new Requests_IRI($relative); - } - if (!$relative->is_valid()) - { - return false; - } - elseif ($relative->scheme !== null) - { - return clone $relative; - } - else - { - if (!($base instanceof Requests_IRI)) - { - $base = new Requests_IRI($base); - } - if ($base->scheme !== null && $base->is_valid()) - { - if ($relative->get_iri() !== '') - { - if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) - { - $target = clone $relative; - $target->scheme = $base->scheme; - } - else - { - $target = new Requests_IRI; - $target->scheme = $base->scheme; - $target->iuserinfo = $base->iuserinfo; - $target->ihost = $base->ihost; - $target->port = $base->port; - if ($relative->ipath !== '') - { - if ($relative->ipath[0] === '/') - { - $target->ipath = $relative->ipath; - } - elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') - { - $target->ipath = '/' . $relative->ipath; - } - elseif (($last_segment = strrpos($base->ipath, '/')) !== false) - { - $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; - } - else - { - $target->ipath = $relative->ipath; - } - $target->ipath = $target->remove_dot_segments($target->ipath); - $target->iquery = $relative->iquery; - } - else - { - $target->ipath = $base->ipath; - if ($relative->iquery !== null) - { - $target->iquery = $relative->iquery; - } - elseif ($base->iquery !== null) - { - $target->iquery = $base->iquery; - } - } - $target->ifragment = $relative->ifragment; - } - } - else - { - $target = clone $base; - $target->ifragment = null; - } - $target->scheme_normalization(); - return $target; - } - else - { - return false; - } - } - } - - /** - * Parse an IRI into scheme/authority/path/query/fragment segments - * - * @param string $iri - * @return array - */ - protected function parse_iri($iri) - { - $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); - if (preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match)) - { - if ($match[1] === '') - { - $match['scheme'] = null; - } - if (!isset($match[3]) || $match[3] === '') - { - $match['authority'] = null; - } - if (!isset($match[5])) - { - $match['path'] = ''; - } - if (!isset($match[6]) || $match[6] === '') - { - $match['query'] = null; - } - if (!isset($match[8]) || $match[8] === '') - { - $match['fragment'] = null; - } - return $match; - } - else - { - trigger_error('This should never happen', E_USER_ERROR); - die; - } - } - - /** - * Remove dot segments from a path - * - * @param string $input - * @return string - */ - protected function remove_dot_segments($input) - { - $output = ''; - while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') - { - // A: If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise, - if (strpos($input, '../') === 0) - { - $input = substr($input, 3); - } - elseif (strpos($input, './') === 0) - { - $input = substr($input, 2); - } - // B: if the input buffer begins with a prefix of "/./" or "/.", where "." is a complete path segment, then replace that prefix with "/" in the input buffer; otherwise, - elseif (strpos($input, '/./') === 0) - { - $input = substr($input, 2); - } - elseif ($input === '/.') - { - $input = '/'; - } - // C: if the input buffer begins with a prefix of "/../" or "/..", where ".." is a complete path segment, then replace that prefix with "/" in the input buffer and remove the last segment and its preceding "/" (if any) from the output buffer; otherwise, - elseif (strpos($input, '/../') === 0) - { - $input = substr($input, 3); - $output = substr_replace($output, '', strrpos($output, '/')); - } - elseif ($input === '/..') - { - $input = '/'; - $output = substr_replace($output, '', strrpos($output, '/')); - } - // D: if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise, - elseif ($input === '.' || $input === '..') - { - $input = ''; - } - // E: move the first path segment in the input buffer to the end of the output buffer, including the initial "/" character (if any) and any subsequent characters up to, but not including, the next "/" character or the end of the input buffer - elseif (($pos = strpos($input, '/', 1)) !== false) - { - $output .= substr($input, 0, $pos); - $input = substr_replace($input, '', 0, $pos); - } - else - { - $output .= $input; - $input = ''; - } - } - return $output . $input; - } - - /** - * Replace invalid character with percent encoding - * - * @param string $string Input string - * @param string $extra_chars Valid characters not in iunreserved or - * iprivate (this is ASCII-only) - * @param bool $iprivate Allow iprivate - * @return string - */ - protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) - { - // Normalize as many pct-encoded sections as possible - $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array(&$this, 'remove_iunreserved_percent_encoded'), $string); - - // Replace invalid percent characters - $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); - - // Add unreserved and % to $extra_chars (the latter is safe because all - // pct-encoded sections are now valid). - $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; - - // Now replace any bytes that aren't allowed with their pct-encoded versions - $position = 0; - $strlen = strlen($string); - while (($position += strspn($string, $extra_chars, $position)) < $strlen) - { - $value = ord($string[$position]); - - // Start position - $start = $position; - - // By default we are valid - $valid = true; - - // No one byte sequences are valid due to the while. - // Two byte sequence: - if (($value & 0xE0) === 0xC0) - { - $character = ($value & 0x1F) << 6; - $length = 2; - $remaining = 1; - } - // Three byte sequence: - elseif (($value & 0xF0) === 0xE0) - { - $character = ($value & 0x0F) << 12; - $length = 3; - $remaining = 2; - } - // Four byte sequence: - elseif (($value & 0xF8) === 0xF0) - { - $character = ($value & 0x07) << 18; - $length = 4; - $remaining = 3; - } - // Invalid byte: - else - { - $valid = false; - $length = 1; - $remaining = 0; - } - - if ($remaining) - { - if ($position + $length <= $strlen) - { - for ($position++; $remaining; $position++) - { - $value = ord($string[$position]); - - // Check that the byte is valid, then add it to the character: - if (($value & 0xC0) === 0x80) - { - $character |= ($value & 0x3F) << (--$remaining * 6); - } - // If it is invalid, count the sequence as invalid and reprocess the current byte: - else - { - $valid = false; - $position--; - break; - } - } - } - else - { - $position = $strlen - 1; - $valid = false; - } - } - - // Percent encode anything invalid or not in ucschar - if ( - // Invalid sequences - !$valid - // 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 < 0xA0 - || $character > 0xEFFFD - ) - && ( - // Everything not in iprivate, if it applies - !$iprivate - || $character < 0xE000 - || $character > 0x10FFFD - ) - ) - { - // If we were a character, pretend we weren't, but rather an error. - if ($valid) - $position--; - - for ($j = $start; $j <= $position; $j++) - { - $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); - $j += 2; - $position += 2; - $strlen += 2; - } - } - } - - return $string; - } - - /** - * Callback function for preg_replace_callback. - * - * Removes sequences of percent encoded bytes that represent UTF-8 - * encoded characters in iunreserved - * - * @param array $match PCRE match - * @return string Replacement - */ - protected function remove_iunreserved_percent_encoded($match) - { - // As we just have valid percent encoded sequences we can just explode - // and ignore the first member of the returned array (an empty string). - $bytes = explode('%', $match[0]); - - // Initialize the new string (this is what will be returned) and that - // there are no bytes remaining in the current sequence (unsurprising - // at the first byte!). - $string = ''; - $remaining = 0; - - // Loop over each and every byte, and set $value to its value - for ($i = 1, $len = count($bytes); $i < $len; $i++) - { - $value = hexdec($bytes[$i]); - - // If we're the first byte of sequence: - if (!$remaining) - { - // Start position - $start = $i; - - // By default we are valid - $valid = true; - - // One byte sequence: - if ($value <= 0x7F) - { - $character = $value; - $length = 1; - } - // Two byte sequence: - elseif (($value & 0xE0) === 0xC0) - { - $character = ($value & 0x1F) << 6; - $length = 2; - $remaining = 1; - } - // Three byte sequence: - elseif (($value & 0xF0) === 0xE0) - { - $character = ($value & 0x0F) << 12; - $length = 3; - $remaining = 2; - } - // Four byte sequence: - elseif (($value & 0xF8) === 0xF0) - { - $character = ($value & 0x07) << 18; - $length = 4; - $remaining = 3; - } - // Invalid byte: - else - { - $valid = false; - $remaining = 0; - } - } - // Continuation byte: - else - { - // Check that the byte is valid, then add it to the character: - if (($value & 0xC0) === 0x80) - { - $remaining--; - $character |= ($value & 0x3F) << ($remaining * 6); - } - // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: - else - { - $valid = false; - $remaining = 0; - $i--; - } - } - - // If we've reached the end of the current byte sequence, append it to Unicode::$data - if (!$remaining) - { - // Percent encode anything invalid or not in iunreserved - if ( - // Invalid sequences - !$valid - // Non-shortest form sequences are invalid - || $length > 1 && $character <= 0x7F - || $length > 2 && $character <= 0x7FF - || $length > 3 && $character <= 0xFFFF - // Outside of range of iunreserved codepoints - || $character < 0x2D - || $character > 0xEFFFD - // Noncharacters - || ($character & 0xFFFE) === 0xFFFE - || $character >= 0xFDD0 && $character <= 0xFDEF - // Everything else not in iunreserved (this is all BMP) - || $character === 0x2F - || $character > 0x39 && $character < 0x41 - || $character > 0x5A && $character < 0x61 - || $character > 0x7A && $character < 0x7E - || $character > 0x7E && $character < 0xA0 - || $character > 0xD7FF && $character < 0xF900 - ) - { - for ($j = $start; $j <= $i; $j++) - { - $string .= '%' . strtoupper($bytes[$j]); - } - } - else - { - for ($j = $start; $j <= $i; $j++) - { - $string .= chr(hexdec($bytes[$j])); - } - } - } - } - - // If we have any bytes left over they are invalid (i.e., we are - // mid-way through a multi-byte sequence) - if ($remaining) - { - for ($j = $start; $j < $len; $j++) - { - $string .= '%' . strtoupper($bytes[$j]); - } - } - - return $string; - } - - protected function scheme_normalization() - { - if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) - { - $this->iuserinfo = null; - } - if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) - { - $this->ihost = null; - } - if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) - { - $this->port = null; - } - if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) - { - $this->ipath = ''; - } - if (isset($this->ihost) && empty($this->ipath)) - { - $this->ipath = '/'; - } - if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) - { - $this->iquery = null; - } - if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) - { - $this->ifragment = null; - } - } - - /** - * Check if the object represents a valid IRI. This needs to be done on each - * call as some things change depending on another part of the IRI. - * - * @return bool - */ - public function is_valid() - { - $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; - if ($this->ipath !== '' && - ( - $isauthority && ( - $this->ipath[0] !== '/' || - substr($this->ipath, 0, 2) === '//' - ) || - ( - $this->scheme === null && - !$isauthority && - strpos($this->ipath, ':') !== false && - (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) - ) - ) - ) - { - return false; - } - - return true; - } - - /** - * Set the entire IRI. Returns true on success, false on failure (if there - * are any invalid characters). - * - * @param string $iri - * @return bool - */ - protected function set_iri($iri) - { - static $cache; - if (!$cache) - { - $cache = array(); - } - - if ($iri === null) - { - return true; - } - elseif (isset($cache[$iri])) - { - list($this->scheme, - $this->iuserinfo, - $this->ihost, - $this->port, - $this->ipath, - $this->iquery, - $this->ifragment, - $return) = $cache[$iri]; - return $return; - } - else - { - $parsed = $this->parse_iri((string) $iri); - - $return = $this->set_scheme($parsed['scheme']) - && $this->set_authority($parsed['authority']) - && $this->set_path($parsed['path']) - && $this->set_query($parsed['query']) - && $this->set_fragment($parsed['fragment']); - - $cache[$iri] = array($this->scheme, - $this->iuserinfo, - $this->ihost, - $this->port, - $this->ipath, - $this->iquery, - $this->ifragment, - $return); - return $return; - } - } - - /** - * Set the scheme. Returns true on success, false on failure (if there are - * any invalid characters). - * - * @param string $scheme - * @return bool - */ - protected function set_scheme($scheme) - { - if ($scheme === null) - { - $this->scheme = null; - } - elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) - { - $this->scheme = null; - return false; - } - else - { - $this->scheme = strtolower($scheme); - } - return true; - } - - /** - * Set the authority. Returns true on success, false on failure (if there are - * any invalid characters). - * - * @param string $authority - * @return bool - */ - protected function set_authority($authority) - { - static $cache; - if (!$cache) - $cache = array(); - - if ($authority === null) - { - $this->iuserinfo = null; - $this->ihost = null; - $this->port = null; - return true; - } - elseif (isset($cache[$authority])) - { - list($this->iuserinfo, - $this->ihost, - $this->port, - $return) = $cache[$authority]; - - return $return; - } - else - { - $remaining = $authority; - if (($iuserinfo_end = strrpos($remaining, '@')) !== false) - { - $iuserinfo = substr($remaining, 0, $iuserinfo_end); - $remaining = substr($remaining, $iuserinfo_end + 1); - } - else - { - $iuserinfo = null; - } - if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) - { - if (($port = substr($remaining, $port_start + 1)) === false) - { - $port = null; - } - $remaining = substr($remaining, 0, $port_start); - } - else - { - $port = null; - } - - $return = $this->set_userinfo($iuserinfo) && - $this->set_host($remaining) && - $this->set_port($port); - - $cache[$authority] = array($this->iuserinfo, - $this->ihost, - $this->port, - $return); - - return $return; - } - } - - /** - * Set the iuserinfo. - * - * @param string $iuserinfo - * @return bool - */ - protected function set_userinfo($iuserinfo) - { - if ($iuserinfo === null) - { - $this->iuserinfo = null; - } - else - { - $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); - $this->scheme_normalization(); - } - - return true; - } - - /** - * Set the ihost. Returns true on success, false on failure (if there are - * any invalid characters). - * - * @param string $ihost - * @return bool - */ - protected function set_host($ihost) - { - if ($ihost === null) - { - $this->ihost = null; - return true; - } - elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') - { - if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) - { - $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']'; - } - else - { - $this->ihost = null; - return false; - } - } - else - { - $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); - - // Lowercase, but ignore pct-encoded sections (as they should - // remain uppercase). This must be done after the previous step - // as that can add unescaped characters. - $position = 0; - $strlen = strlen($ihost); - while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) - { - if ($ihost[$position] === '%') - { - $position += 3; - } - else - { - $ihost[$position] = strtolower($ihost[$position]); - $position++; - } - } - - $this->ihost = $ihost; - } - - $this->scheme_normalization(); - - return true; - } - - /** - * Set the port. Returns true on success, false on failure (if there are - * any invalid characters). - * - * @param string $port - * @return bool - */ - protected function set_port($port) - { - if ($port === null) - { - $this->port = null; - return true; - } - elseif (strspn($port, '0123456789') === strlen($port)) - { - $this->port = (int) $port; - $this->scheme_normalization(); - return true; - } - else - { - $this->port = null; - return false; - } - } - - /** - * Set the ipath. - * - * @param string $ipath - * @return bool - */ - protected function set_path($ipath) - { - static $cache; - if (!$cache) - { - $cache = array(); - } - - $ipath = (string) $ipath; - - if (isset($cache[$ipath])) - { - $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; - } - else - { - $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); - $removed = $this->remove_dot_segments($valid); - - $cache[$ipath] = array($valid, $removed); - $this->ipath = ($this->scheme !== null) ? $removed : $valid; - } - $this->scheme_normalization(); - return true; - } - - /** - * Set the iquery. - * - * @param string $iquery - * @return bool - */ - protected function set_query($iquery) - { - if ($iquery === null) - { - $this->iquery = null; - } - else - { - $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); - $this->scheme_normalization(); - } - return true; - } - - /** - * Set the ifragment. - * - * @param string $ifragment - * @return bool - */ - protected function set_fragment($ifragment) - { - if ($ifragment === null) - { - $this->ifragment = null; - } - else - { - $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); - $this->scheme_normalization(); - } - return true; - } - - /** - * Convert an IRI to a URI (or parts thereof) - * - * @return string - */ - protected function to_uri($string) - { - static $non_ascii; - if (!$non_ascii) - { - $non_ascii = implode('', range("\x80", "\xFF")); - } - - $position = 0; - $strlen = strlen($string); - while (($position += strcspn($string, $non_ascii, $position)) < $strlen) - { - $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); - $position += 3; - $strlen += 2; - } - - return $string; - } - - /** - * Get the complete IRI - * - * @return string - */ - protected function get_iri() - { - if (!$this->is_valid()) - { - return false; - } - - $iri = ''; - if ($this->scheme !== null) - { - $iri .= $this->scheme . ':'; - } - if (($iauthority = $this->get_iauthority()) !== null) - { - $iri .= '//' . $iauthority; - } - $iri .= $this->ipath; - if ($this->iquery !== null) - { - $iri .= '?' . $this->iquery; - } - if ($this->ifragment !== null) - { - $iri .= '#' . $this->ifragment; - } - - return $iri; - } - - /** - * Get the complete URI - * - * @return string - */ - protected function get_uri() - { - return $this->to_uri($this->get_iri()); - } - - /** - * Get the complete iauthority - * - * @return string - */ - protected function get_iauthority() - { - if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null) - { - $iauthority = ''; - if ($this->iuserinfo !== null) - { - $iauthority .= $this->iuserinfo . '@'; - } - if ($this->ihost !== null) - { - $iauthority .= $this->ihost; - } - if ($this->port !== null) - { - $iauthority .= ':' . $this->port; - } - return $iauthority; - } - else - { - return null; - } - } - - /** - * Get the complete authority - * - * @return string - */ - protected function get_authority() - { - $iauthority = $this->get_iauthority(); - if (is_string($iauthority)) - return $this->to_uri($iauthority); - else - return $iauthority; - } +class Requests_IRI { + /** + * Scheme + * + * @var string + */ + protected $scheme = null; + + /** + * User Information + * + * @var string + */ + protected $iuserinfo = null; + + /** + * ihost + * + * @var string + */ + protected $ihost = null; + + /** + * Port + * + * @var string + */ + protected $port = null; + + /** + * ipath + * + * @var string + */ + protected $ipath = ''; + + /** + * iquery + * + * @var string + */ + protected $iquery = null; + + /** + * ifragment + * + * @var string + */ + protected $ifragment = null; + + /** + * Normalization database + * + * Each key is the scheme, each value is an array with each key as the IRI + * part and value as the default value for that part. + */ + protected $normalization = array( + 'acap' => array( + 'port' => 674 + ), + 'dict' => array( + 'port' => 2628 + ), + 'file' => array( + 'ihost' => 'localhost' + ), + 'http' => array( + 'port' => 80, + ), + 'https' => array( + 'port' => 443, + ), + ); + + /** + * Return the entire IRI when you try and read the object as a string + * + * @return string + */ + public function __toString() { + return $this->get_iri(); + } + + /** + * Overload __set() to provide access via properties + * + * @param string $name Property name + * @param mixed $value Property value + */ + public function __set($name, $value) { + if (method_exists($this, 'set_' . $name)) { + call_user_func(array($this, 'set_' . $name), $value); + } + elseif ( + $name === 'iauthority' + || $name === 'iuserinfo' + || $name === 'ihost' + || $name === 'ipath' + || $name === 'iquery' + || $name === 'ifragment' + ) { + call_user_func(array($this, 'set_' . substr($name, 1)), $value); + } + } + + /** + * Overload __get() to provide access via properties + * + * @param string $name Property name + * @return mixed + */ + public function __get($name) { + // isset() returns false for null, we don't want to do that + // Also why we use array_key_exists below instead of isset() + $props = get_object_vars($this); + + if ( + $name === 'iri' || + $name === 'uri' || + $name === 'iauthority' || + $name === 'authority' + ) { + $method = 'get_' . $name; + $return = $this->$method(); + } + elseif (array_key_exists($name, $props)) { + $return = $this->$name; + } + // host -> ihost + elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) { + $name = $prop; + $return = $this->$prop; + } + // ischeme -> scheme + elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) { + $name = $prop; + $return = $this->$prop; + } + else { + trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); + $return = null; + } + + if ($return === null && isset($this->normalization[$this->scheme][$name])) { + return $this->normalization[$this->scheme][$name]; + } + else { + return $return; + } + } + + /** + * Overload __isset() to provide access via properties + * + * @param string $name Property name + * @return bool + */ + public function __isset($name) { + return (method_exists($this, 'get_' . $name) || isset($this->$name)); + } + + /** + * Overload __unset() to provide access via properties + * + * @param string $name Property name + */ + public function __unset($name) { + if (method_exists($this, 'set_' . $name)) { + call_user_func(array($this, 'set_' . $name), ''); + } + } + + /** + * Create a new IRI object, from a specified string + * + * @param string|null $iri + */ + public function __construct($iri = null) { + $this->set_iri($iri); + } + + /** + * Create a new IRI object by resolving a relative IRI + * + * Returns false if $base is not absolute, otherwise an IRI. + * + * @param IRI|string $base (Absolute) Base IRI + * @param IRI|string $relative Relative IRI + * @return IRI|false + */ + public static function absolutize($base, $relative) { + if (!($relative instanceof Requests_IRI)) { + $relative = new Requests_IRI($relative); + } + if (!$relative->is_valid()) { + return false; + } + elseif ($relative->scheme !== null) { + return clone $relative; + } + + if (!($base instanceof Requests_IRI)) { + $base = new Requests_IRI($base); + } + if ($base->scheme === null || !$base->is_valid()) { + return false; + } + + if ($relative->get_iri() !== '') { + if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) { + $target = clone $relative; + $target->scheme = $base->scheme; + } + else { + $target = new Requests_IRI; + $target->scheme = $base->scheme; + $target->iuserinfo = $base->iuserinfo; + $target->ihost = $base->ihost; + $target->port = $base->port; + if ($relative->ipath !== '') { + if ($relative->ipath[0] === '/') { + $target->ipath = $relative->ipath; + } + elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') { + $target->ipath = '/' . $relative->ipath; + } + elseif (($last_segment = strrpos($base->ipath, '/')) !== false) { + $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; + } + else { + $target->ipath = $relative->ipath; + } + $target->ipath = $target->remove_dot_segments($target->ipath); + $target->iquery = $relative->iquery; + } + else { + $target->ipath = $base->ipath; + if ($relative->iquery !== null) { + $target->iquery = $relative->iquery; + } + elseif ($base->iquery !== null) { + $target->iquery = $base->iquery; + } + } + $target->ifragment = $relative->ifragment; + } + } + else { + $target = clone $base; + $target->ifragment = null; + } + $target->scheme_normalization(); + return $target; + } + + /** + * Parse an IRI into scheme/authority/path/query/fragment segments + * + * @param string $iri + * @return array + */ + protected function parse_iri($iri) { + $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); + $has_match = preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match); + if (!$has_match) { + throw new Requests_Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); + } + + if ($match[1] === '') { + $match['scheme'] = null; + } + if (!isset($match[3]) || $match[3] === '') { + $match['authority'] = null; + } + if (!isset($match[5])) { + $match['path'] = ''; + } + if (!isset($match[6]) || $match[6] === '') { + $match['query'] = null; + } + if (!isset($match[8]) || $match[8] === '') { + $match['fragment'] = null; + } + return $match; + } + + /** + * Remove dot segments from a path + * + * @param string $input + * @return string + */ + protected function remove_dot_segments($input) { + $output = ''; + while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') { + // A: If the input buffer begins with a prefix of "../" or "./", + // then remove that prefix from the input buffer; otherwise, + if (strpos($input, '../') === 0) { + $input = substr($input, 3); + } + elseif (strpos($input, './') === 0) { + $input = substr($input, 2); + } + // B: if the input buffer begins with a prefix of "/./" or "/.", + // where "." is a complete path segment, then replace that prefix + // with "/" in the input buffer; otherwise, + elseif (strpos($input, '/./') === 0) { + $input = substr($input, 2); + } + elseif ($input === '/.') { + $input = '/'; + } + // C: if the input buffer begins with a prefix of "/../" or "/..", + // where ".." is a complete path segment, then replace that prefix + // with "/" in the input buffer and remove the last segment and its + // preceding "/" (if any) from the output buffer; otherwise, + elseif (strpos($input, '/../') === 0) { + $input = substr($input, 3); + $output = substr_replace($output, '', strrpos($output, '/')); + } + elseif ($input === '/..') { + $input = '/'; + $output = substr_replace($output, '', strrpos($output, '/')); + } + // D: if the input buffer consists only of "." or "..", then remove + // that from the input buffer; otherwise, + elseif ($input === '.' || $input === '..') { + $input = ''; + } + // E: move the first path segment in the input buffer to the end of + // the output buffer, including the initial "/" character (if any) + // and any subsequent characters up to, but not including, the next + // "/" character or the end of the input buffer + elseif (($pos = strpos($input, '/', 1)) !== false) { + $output .= substr($input, 0, $pos); + $input = substr_replace($input, '', 0, $pos); + } + else { + $output .= $input; + $input = ''; + } + } + return $output . $input; + } + + /** + * Replace invalid character with percent encoding + * + * @param string $string Input string + * @param string $extra_chars Valid characters not in iunreserved or + * iprivate (this is ASCII-only) + * @param bool $iprivate Allow iprivate + * @return string + */ + protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) { + // Normalize as many pct-encoded sections as possible + $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array(&$this, 'remove_iunreserved_percent_encoded'), $string); + + // Replace invalid percent characters + $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); + + // Add unreserved and % to $extra_chars (the latter is safe because all + // pct-encoded sections are now valid). + $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; + + // Now replace any bytes that aren't allowed with their pct-encoded versions + $position = 0; + $strlen = strlen($string); + while (($position += strspn($string, $extra_chars, $position)) < $strlen) { + $value = ord($string[$position]); + + // Start position + $start = $position; + + // By default we are valid + $valid = true; + + // No one byte sequences are valid due to the while. + // Two byte sequence: + if (($value & 0xE0) === 0xC0) { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else { + $valid = false; + $length = 1; + $remaining = 0; + } + + if ($remaining) { + if ($position + $length <= $strlen) { + for ($position++; $remaining; $position++) { + $value = ord($string[$position]); + + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) { + $character |= ($value & 0x3F) << (--$remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte: + else { + $valid = false; + $position--; + break; + } + } + } + else { + $position = $strlen - 1; + $valid = false; + } + } + + // Percent encode anything invalid or not in ucschar + if ( + // Invalid sequences + !$valid + // 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 < 0xA0 + || $character > 0xEFFFD + ) + && ( + // Everything not in iprivate, if it applies + !$iprivate + || $character < 0xE000 + || $character > 0x10FFFD + ) + ) { + // If we were a character, pretend we weren't, but rather an error. + if ($valid) { + $position--; + } + + for ($j = $start; $j <= $position; $j++) { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); + $j += 2; + $position += 2; + $strlen += 2; + } + } + } + + return $string; + } + + /** + * Callback function for preg_replace_callback. + * + * Removes sequences of percent encoded bytes that represent UTF-8 + * encoded characters in iunreserved + * + * @param array $match PCRE match + * @return string Replacement + */ + protected function remove_iunreserved_percent_encoded($match) { + // As we just have valid percent encoded sequences we can just explode + // and ignore the first member of the returned array (an empty string). + $bytes = explode('%', $match[0]); + + // Initialize the new string (this is what will be returned) and that + // there are no bytes remaining in the current sequence (unsurprising + // at the first byte!). + $string = ''; + $remaining = 0; + + // Loop over each and every byte, and set $value to its value + for ($i = 1, $len = count($bytes); $i < $len; $i++) { + $value = hexdec($bytes[$i]); + + // If we're the first byte of sequence: + if (!$remaining) { + // Start position + $start = $i; + + // By default we are valid + $valid = true; + + // One byte sequence: + if ($value <= 0x7F) { + $character = $value; + $length = 1; + } + // Two byte sequence: + elseif (($value & 0xE0) === 0xC0) { + $character = ($value & 0x1F) << 6; + $length = 2; + $remaining = 1; + } + // Three byte sequence: + elseif (($value & 0xF0) === 0xE0) { + $character = ($value & 0x0F) << 12; + $length = 3; + $remaining = 2; + } + // Four byte sequence: + elseif (($value & 0xF8) === 0xF0) { + $character = ($value & 0x07) << 18; + $length = 4; + $remaining = 3; + } + // Invalid byte: + else { + $valid = false; + $remaining = 0; + } + } + // Continuation byte: + else { + // Check that the byte is valid, then add it to the character: + if (($value & 0xC0) === 0x80) { + $remaining--; + $character |= ($value & 0x3F) << ($remaining * 6); + } + // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: + else { + $valid = false; + $remaining = 0; + $i--; + } + } + + // If we've reached the end of the current byte sequence, append it to Unicode::$data + if (!$remaining) { + // Percent encode anything invalid or not in iunreserved + if ( + // Invalid sequences + !$valid + // Non-shortest form sequences are invalid + || $length > 1 && $character <= 0x7F + || $length > 2 && $character <= 0x7FF + || $length > 3 && $character <= 0xFFFF + // Outside of range of iunreserved codepoints + || $character < 0x2D + || $character > 0xEFFFD + // Noncharacters + || ($character & 0xFFFE) === 0xFFFE + || $character >= 0xFDD0 && $character <= 0xFDEF + // Everything else not in iunreserved (this is all BMP) + || $character === 0x2F + || $character > 0x39 && $character < 0x41 + || $character > 0x5A && $character < 0x61 + || $character > 0x7A && $character < 0x7E + || $character > 0x7E && $character < 0xA0 + || $character > 0xD7FF && $character < 0xF900 + ) { + for ($j = $start; $j <= $i; $j++) { + $string .= '%' . strtoupper($bytes[$j]); + } + } + else { + for ($j = $start; $j <= $i; $j++) { + $string .= chr(hexdec($bytes[$j])); + } + } + } + } + + // If we have any bytes left over they are invalid (i.e., we are + // mid-way through a multi-byte sequence) + if ($remaining) { + for ($j = $start; $j < $len; $j++) { + $string .= '%' . strtoupper($bytes[$j]); + } + } + + return $string; + } + + protected function scheme_normalization() { + if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { + $this->iuserinfo = null; + } + if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) { + $this->ihost = null; + } + if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) { + $this->port = null; + } + if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) { + $this->ipath = ''; + } + if (isset($this->ihost) && empty($this->ipath)) { + $this->ipath = '/'; + } + if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) { + $this->iquery = null; + } + if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) { + $this->ifragment = null; + } + } + + /** + * Check if the object represents a valid IRI. This needs to be done on each + * call as some things change depending on another part of the IRI. + * + * @return bool + */ + public function is_valid() { + $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; + if ($this->ipath !== '' && + ( + $isauthority && ( + $this->ipath[0] !== '/' || + substr($this->ipath, 0, 2) === '//' + ) || + ( + $this->scheme === null && + !$isauthority && + strpos($this->ipath, ':') !== false && + (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) + ) + ) + ) { + return false; + } + + return true; + } + + /** + * Set the entire IRI. Returns true on success, false on failure (if there + * are any invalid characters). + * + * @param string $iri + * @return bool + */ + protected function set_iri($iri) { + static $cache; + if (!$cache) { + $cache = array(); + } + + if ($iri === null) { + return true; + } + if (isset($cache[$iri])) { + list($this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return) = $cache[$iri]; + return $return; + } + + $parsed = $this->parse_iri((string) $iri); + + $return = $this->set_scheme($parsed['scheme']) + && $this->set_authority($parsed['authority']) + && $this->set_path($parsed['path']) + && $this->set_query($parsed['query']) + && $this->set_fragment($parsed['fragment']); + + $cache[$iri] = array($this->scheme, + $this->iuserinfo, + $this->ihost, + $this->port, + $this->ipath, + $this->iquery, + $this->ifragment, + $return); + return $return; + } + + /** + * Set the scheme. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $scheme + * @return bool + */ + protected function set_scheme($scheme) { + if ($scheme === null) { + $this->scheme = null; + } + elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) { + $this->scheme = null; + return false; + } + else { + $this->scheme = strtolower($scheme); + } + return true; + } + + /** + * Set the authority. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $authority + * @return bool + */ + protected function set_authority($authority) { + static $cache; + if (!$cache) { + $cache = array(); + } + + if ($authority === null) { + $this->iuserinfo = null; + $this->ihost = null; + $this->port = null; + return true; + } + if (isset($cache[$authority])) { + list($this->iuserinfo, + $this->ihost, + $this->port, + $return) = $cache[$authority]; + + return $return; + } + + $remaining = $authority; + if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { + $iuserinfo = substr($remaining, 0, $iuserinfo_end); + $remaining = substr($remaining, $iuserinfo_end + 1); + } + else { + $iuserinfo = null; + } + if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) { + $port = substr($remaining, $port_start + 1); + if ($port === false || $port === '') { + $port = null; + } + $remaining = substr($remaining, 0, $port_start); + } + else { + $port = null; + } + + $return = $this->set_userinfo($iuserinfo) && + $this->set_host($remaining) && + $this->set_port($port); + + $cache[$authority] = array($this->iuserinfo, + $this->ihost, + $this->port, + $return); + + return $return; + } + + /** + * Set the iuserinfo. + * + * @param string $iuserinfo + * @return bool + */ + protected function set_userinfo($iuserinfo) { + if ($iuserinfo === null) { + $this->iuserinfo = null; + } + else { + $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); + $this->scheme_normalization(); + } + + return true; + } + + /** + * Set the ihost. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $ihost + * @return bool + */ + protected function set_host($ihost) { + if ($ihost === null) { + $this->ihost = null; + return true; + } + if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { + if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) { + $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']'; + } + else { + $this->ihost = null; + return false; + } + } + else { + $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); + + // Lowercase, but ignore pct-encoded sections (as they should + // remain uppercase). This must be done after the previous step + // as that can add unescaped characters. + $position = 0; + $strlen = strlen($ihost); + while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) { + if ($ihost[$position] === '%') { + $position += 3; + } + else { + $ihost[$position] = strtolower($ihost[$position]); + $position++; + } + } + + $this->ihost = $ihost; + } + + $this->scheme_normalization(); + + return true; + } + + /** + * Set the port. Returns true on success, false on failure (if there are + * any invalid characters). + * + * @param string $port + * @return bool + */ + protected function set_port($port) { + if ($port === null) { + $this->port = null; + return true; + } + + if (strspn($port, '0123456789') === strlen($port)) { + $this->port = (int) $port; + $this->scheme_normalization(); + return true; + } + + $this->port = null; + return false; + } + + /** + * Set the ipath. + * + * @param string $ipath + * @return bool + */ + protected function set_path($ipath) { + static $cache; + if (!$cache) { + $cache = array(); + } + + $ipath = (string) $ipath; + + if (isset($cache[$ipath])) { + $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; + } + else { + $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); + $removed = $this->remove_dot_segments($valid); + + $cache[$ipath] = array($valid, $removed); + $this->ipath = ($this->scheme !== null) ? $removed : $valid; + } + $this->scheme_normalization(); + return true; + } + + /** + * Set the iquery. + * + * @param string $iquery + * @return bool + */ + protected function set_query($iquery) { + if ($iquery === null) { + $this->iquery = null; + } + else { + $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); + $this->scheme_normalization(); + } + return true; + } + + /** + * Set the ifragment. + * + * @param string $ifragment + * @return bool + */ + protected function set_fragment($ifragment) { + if ($ifragment === null) { + $this->ifragment = null; + } + else { + $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); + $this->scheme_normalization(); + } + return true; + } + + /** + * Convert an IRI to a URI (or parts thereof) + * + * @param string|bool IRI to convert (or false from {@see get_iri}) + * @return string|false URI if IRI is valid, false otherwise. + */ + protected function to_uri($string) { + if (!is_string($string)) { + return false; + } + + static $non_ascii; + if (!$non_ascii) { + $non_ascii = implode('', range("\x80", "\xFF")); + } + + $position = 0; + $strlen = strlen($string); + while (($position += strcspn($string, $non_ascii, $position)) < $strlen) { + $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); + $position += 3; + $strlen += 2; + } + + return $string; + } + + /** + * Get the complete IRI + * + * @return string + */ + protected function get_iri() { + if (!$this->is_valid()) { + return false; + } + + $iri = ''; + if ($this->scheme !== null) { + $iri .= $this->scheme . ':'; + } + if (($iauthority = $this->get_iauthority()) !== null) { + $iri .= '//' . $iauthority; + } + $iri .= $this->ipath; + if ($this->iquery !== null) { + $iri .= '?' . $this->iquery; + } + if ($this->ifragment !== null) { + $iri .= '#' . $this->ifragment; + } + + return $iri; + } + + /** + * Get the complete URI + * + * @return string + */ + protected function get_uri() { + return $this->to_uri($this->get_iri()); + } + + /** + * Get the complete iauthority + * + * @return string + */ + protected function get_iauthority() { + if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) { + return null; + } + + $iauthority = ''; + if ($this->iuserinfo !== null) { + $iauthority .= $this->iuserinfo . '@'; + } + if ($this->ihost !== null) { + $iauthority .= $this->ihost; + } + if ($this->port !== null) { + $iauthority .= ':' . $this->port; + } + return $iauthority; + } + + /** + * Get the complete authority + * + * @return string + */ + protected function get_authority() { + $iauthority = $this->get_iauthority(); + if (is_string($iauthority)) { + return $this->to_uri($iauthority); + } + else { + return $iauthority; + } + } } diff --git a/includes/Requests/Requests/Proxy/HTTP.php b/includes/Requests/Requests/Proxy/HTTP.php index 1311f6f..6b4aba8 100644 --- a/includes/Requests/Requests/Proxy/HTTP.php +++ b/includes/Requests/Requests/Proxy/HTTP.php @@ -67,7 +67,7 @@ public function __construct($args = null) { $this->use_authentication = true; } else { - throw new Requests_Exception( 'Invalid number of arguments', 'proxyhttpbadargs'); + throw new Requests_Exception('Invalid number of arguments', 'proxyhttpbadargs'); } } } @@ -87,7 +87,7 @@ public function register(Requests_Hooks &$hooks) { $hooks->register('fsockopen.remote_socket', array(&$this, 'fsockopen_remote_socket')); $hooks->register('fsockopen.remote_host_path', array(&$this, 'fsockopen_remote_host_path')); - if( $this->use_authentication ) { + if ($this->use_authentication) { $hooks->register('fsockopen.after_headers', array(&$this, 'fsockopen_header')); } } @@ -112,9 +112,9 @@ public function curl_before_send(&$handle) { * Alter remote socket information before opening socket connection * * @since 1.6 - * @param string $out HTTP header string + * @param string $remote_socket Socket connection string */ - public function fsockopen_remote_socket( &$remote_socket ) { + public function fsockopen_remote_socket(&$remote_socket) { $remote_socket = $this->proxy; } @@ -122,9 +122,10 @@ public function fsockopen_remote_socket( &$remote_socket ) { * Alter remote path before getting stream data * * @since 1.6 - * @param string $out HTTP header string + * @param string $path Path to send in HTTP request string ("GET ...") + * @param string $url Full URL we're requesting */ - public function fsockopen_remote_host_path( &$path, $url ) { + public function fsockopen_remote_host_path(&$path, $url) { $path = $url; } @@ -134,8 +135,8 @@ public function fsockopen_remote_host_path( &$path, $url ) { * @since 1.6 * @param string $out HTTP header string */ - public function fsockopen_header( &$out ) { - $out .= "Proxy-Authorization: Basic " . base64_encode($this->get_auth_string()) . "\r\n"; + public function fsockopen_header(&$out) { + $out .= sprintf("Proxy-Authorization: Basic %s\r\n", base64_encode($this->get_auth_string())); } /** diff --git a/includes/Requests/Requests/Response.php b/includes/Requests/Requests/Response.php index 684d2d6..3152fb6 100644 --- a/includes/Requests/Requests/Response.php +++ b/includes/Requests/Requests/Response.php @@ -18,61 +18,88 @@ class Requests_Response { */ public function __construct() { $this->headers = new Requests_Response_Headers(); + $this->cookies = new Requests_Cookie_Jar(); } /** * Response body + * * @var string */ public $body = ''; /** * Raw HTTP data from the transport + * * @var string */ public $raw = ''; /** * Headers, as an associative array - * @var array + * + * @var Requests_Response_Headers Array-like object representing headers */ public $headers = array(); /** * Status code, false if non-blocking + * * @var integer|boolean */ public $status_code = false; + /** + * Protocol version, false if non-blocking + * @var float|boolean + */ + public $protocol_version = false; + /** * Whether the request succeeded or not + * * @var boolean */ public $success = false; /** * Number of redirects the request used + * * @var integer */ public $redirects = 0; /** * URL requested + * * @var string */ public $url = ''; /** * Previous requests (from redirects) + * * @var array Array of Requests_Response objects */ public $history = array(); /** * Cookies from the request + * + * @var Requests_Cookie_Jar Array-like object representing a cookie jar */ public $cookies = array(); + /** + * Is the response a redirect? + * + * @return boolean True if redirect (3xx status), false if not. + */ + public function is_redirect() { + $code = $this->status_code; + return in_array($code, array(300, 301, 302, 303, 307)) || $code > 307 && $code < 400; + } + /** * Throws an exception if the request was not successful * @@ -81,15 +108,14 @@ public function __construct() { * @param boolean $allow_redirects Set to false to throw on a 3xx as well */ public function throw_for_status($allow_redirects = true) { - if ($this->status_code >= 300 && $this->status_code < 400) { + if ($this->is_redirect()) { if (!$allow_redirects) { throw new Requests_Exception('Redirection not allowed', 'response.no_redirects', $this); } } - elseif (!$this->success) { $exception = Requests_Exception_HTTP::get_class($this->status_code); throw new $exception(null, $this); } } -} \ No newline at end of file +} diff --git a/includes/Requests/Requests/Response/Headers.php b/includes/Requests/Requests/Response/Headers.php index aa90725..cc6a208 100644 --- a/includes/Requests/Requests/Response/Headers.php +++ b/includes/Requests/Requests/Response/Headers.php @@ -25,8 +25,9 @@ class Requests_Response_Headers extends Requests_Utility_CaseInsensitiveDictiona */ public function offsetGet($key) { $key = strtolower($key); - if (!isset($this->data[$key])) + if (!isset($this->data[$key])) { return null; + } return $this->flatten($this->data[$key]); } @@ -61,8 +62,9 @@ public function offsetSet($key, $value) { */ public function getValues($key) { $key = strtolower($key); - if (!isset($this->data[$key])) + if (!isset($this->data[$key])) { return null; + } return $this->data[$key]; } @@ -77,8 +79,9 @@ public function getValues($key) { * @return string Flattened value */ public function flatten($value) { - if (is_array($value)) + if (is_array($value)) { $value = implode(',', $value); + } return $value; } diff --git a/includes/Requests/Requests/SSL.php b/includes/Requests/Requests/SSL.php index 1ddd894..c227f75 100644 --- a/includes/Requests/Requests/SSL.php +++ b/includes/Requests/Requests/SSL.php @@ -26,7 +26,7 @@ class Requests_SSL { * * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) * @param string $host Host name to verify against - * @param resource $context Stream context + * @param array $cert Certificate data from openssl_x509_parse() * @return bool */ public static function verify_certificate($host, $cert) { @@ -44,8 +44,9 @@ public static function verify_certificate($host, $cert) { $altnames = explode(',', $cert['extensions']['subjectAltName']); foreach ($altnames as $altname) { $altname = trim($altname); - if (strpos($altname, 'DNS:') !== 0) + if (strpos($altname, 'DNS:') !== 0) { continue; + } $has_dns_alt = true; diff --git a/includes/Requests/Requests/Session.php b/includes/Requests/Requests/Session.php index f519d8d..2660085 100644 --- a/includes/Requests/Requests/Session.php +++ b/includes/Requests/Requests/Session.php @@ -194,7 +194,7 @@ public function patch($url, $headers, $data = array(), $options = array()) { * * @param string $url URL to request * @param array $headers Extra headers to send with the request - * @param array $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @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 {@see Requests::request}) * @return Requests_Response @@ -239,7 +239,7 @@ protected function merge_request($request, $merge_options = true) { $request['url'] = Requests_IRI::absolutize($this->url, $request['url']); $request['url'] = $request['url']->uri; } - + $request['headers'] = array_merge($this->headers, $request['headers']); if (is_array($request['data']) && is_array($this->data)) { @@ -252,7 +252,7 @@ protected function merge_request($request, $merge_options = true) { // Disallow forcing the type, as that's a per request setting unset($request['options']['type']); } - + return $request; } } diff --git a/includes/Requests/Requests/Transport/cURL.php b/includes/Requests/Requests/Transport/cURL.php index ee25d36..c1ad446 100644 --- a/includes/Requests/Requests/Transport/cURL.php +++ b/includes/Requests/Requests/Transport/cURL.php @@ -49,7 +49,14 @@ class Requests_Transport_cURL implements Requests_Transport { * * @var resource */ - protected $fp; + protected $handle; + + /** + * Hook dispatcher instance + * + * @var Requests_Hooks + */ + protected $hooks; /** * Have we finished the headers yet? @@ -85,18 +92,27 @@ class Requests_Transport_cURL implements Requests_Transport { public function __construct() { $curl = curl_version(); $this->version = $curl['version_number']; - $this->fp = curl_init(); + $this->handle = curl_init(); - curl_setopt($this->fp, CURLOPT_HEADER, false); - curl_setopt($this->fp, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($this->handle, CURLOPT_HEADER, false); + curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); if ($this->version >= self::CURL_7_10_5) { - curl_setopt($this->fp, CURLOPT_ENCODING, ''); + curl_setopt($this->handle, CURLOPT_ENCODING, ''); } if (defined('CURLOPT_PROTOCOLS')) { - curl_setopt($this->fp, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } if (defined('CURLOPT_REDIR_PROTOCOLS')) { - curl_setopt($this->fp, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + } + + /** + * Destructor + */ + public function __destruct() { + if (is_resource($this->handle)) { + curl_close($this->handle); } } @@ -112,9 +128,11 @@ public function __construct() { * @return string Raw HTTP result */ public function request($url, $headers = array(), $data = array(), $options = array()) { + $this->hooks = $options['hooks']; + $this->setup_handle($url, $headers, $data, $options); - $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); + $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); @@ -129,35 +147,35 @@ public function request($url, $headers = array(), $data = array(), $options = ar if (isset($options['verify'])) { if ($options['verify'] === false) { - curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($this->fp, CURLOPT_SSL_VERIFYPEER, 0); - - } elseif (is_string($options['verify'])) { - curl_setopt($this->fp, CURLOPT_CAINFO, $options['verify']); + curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); + } + elseif (is_string($options['verify'])) { + curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); } } if (isset($options['verifyname']) && $options['verifyname'] === false) { - curl_setopt($this->fp, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); } - curl_exec($this->fp); + curl_exec($this->handle); $response = $this->response_data; - $options['hooks']->dispatch('curl.after_send', array(&$fake_headers)); + $options['hooks']->dispatch('curl.after_send', array()); - if (curl_errno($this->fp) === 23 || curl_errno($this->fp) === 61) { + if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { // Reset encoding and try again - curl_setopt($this->fp, CURLOPT_ENCODING, 'none'); + curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); $this->response_data = ''; $this->response_bytes = 0; - curl_exec($this->fp); + curl_exec($this->handle); $response = $this->response_data; } $this->process_response($response, $options); - curl_close($this->fp); + return $this->headers; } @@ -169,6 +187,11 @@ public function request($url, $headers = array(), $data = array(), $options = ar * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) */ public function request_multiple($requests, $options) { + // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ + if (empty($requests)) { + return array(); + } + $multihandle = curl_multi_init(); $subrequests = array(); $subhandles = array(); @@ -243,7 +266,6 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); - curl_setopt($this->fp, CURLOPT_FILE, $this->stream_handle); } $this->response_data = ''; @@ -252,8 +274,9 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } + $this->hooks = $options['hooks']; - return $this->fp; + return $this->handle; } /** @@ -265,56 +288,82 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param array $options Request options, see {@see Requests::response()} for documentation */ protected function setup_handle($url, $headers, $data, $options) { - $options['hooks']->dispatch('curl.before_request', array(&$this->fp)); + $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); $headers = Requests::flatten($headers); - if (in_array($options['type'], array(Requests::HEAD, Requests::GET, Requests::DELETE)) & !empty($data)) { - $url = self::format_get($url, $data); - } - elseif (!empty($data) && !is_string($data)) { - $data = http_build_query($data, null, '&'); + + if (!empty($data)) { + $data_format = $options['data_format']; + + if ($data_format === 'query') { + $url = self::format_get($url, $data); + $data = ''; + } + elseif (!is_string($data)) { + $data = http_build_query($data, null, '&'); + } } switch ($options['type']) { case Requests::POST: - curl_setopt($this->fp, CURLOPT_POST, true); - curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data); + curl_setopt($this->handle, CURLOPT_POST, true); + curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); break; case Requests::PATCH: case Requests::PUT: - curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, $options['type']); - curl_setopt($this->fp, CURLOPT_POSTFIELDS, $data); - break; case Requests::DELETE: - curl_setopt($this->fp, CURLOPT_CUSTOMREQUEST, 'DELETE'); + case Requests::OPTIONS: + curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); + curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); break; case Requests::HEAD: - curl_setopt($this->fp, CURLOPT_NOBODY, true); + curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); + curl_setopt($this->handle, CURLOPT_NOBODY, true); break; + case Requests::TRACE: + curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); + break; + } + + if (is_int($options['timeout']) || $this->version < self::CURL_7_16_2) { + curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($options['timeout'])); + } + else { + curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($options['timeout'] * 1000)); + } + + if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { + curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); + } + else { + curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); } + curl_setopt($this->handle, CURLOPT_URL, $url); + curl_setopt($this->handle, CURLOPT_REFERER, $url); + curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); + curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); - if( is_int($options['timeout']) or $this->version < self::CURL_7_16_2 ) { - curl_setopt($this->fp, CURLOPT_TIMEOUT, ceil($options['timeout'])); - } else { - curl_setopt($this->fp, CURLOPT_TIMEOUT_MS, round($options['timeout'] * 1000) ); + if ($options['protocol_version'] === 1.1) { + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } - if( is_int($options['connect_timeout']) or $this->version < self::CURL_7_16_2 ) { - curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); - } else { - curl_setopt($this->fp, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); + else { + curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } - curl_setopt($this->fp, CURLOPT_URL, $url); - curl_setopt($this->fp, CURLOPT_REFERER, $url); - curl_setopt($this->fp, CURLOPT_USERAGENT, $options['useragent']); - curl_setopt($this->fp, CURLOPT_HTTPHEADER, $headers); if (true === $options['blocking']) { - curl_setopt($this->fp, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); - curl_setopt($this->fp, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body')); - curl_setopt($this->fp, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); + curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers')); + curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body')); + curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); } } + /** + * Process a response + * + * @param string $response Response data from the body + * @param array $options Request options + * @return string HTTP response data including headers + */ public function process_response($response, $options) { if ($options['blocking'] === false) { $fake_headers = ''; @@ -329,10 +378,15 @@ public function process_response($response, $options) { $this->headers .= $response; } - if (curl_errno($this->fp)) { - throw new Requests_Exception('cURL error ' . curl_errno($this->fp) . ': ' . curl_error($this->fp), 'curlerror', $this->fp); + if (curl_errno($this->handle)) { + $error = sprintf( + 'cURL error %s: %s', + curl_errno($this->handle), + curl_error($this->handle) + ); + throw new Requests_Exception($error, 'curlerror', $this->handle); } - $this->info = curl_getinfo($this->fp); + $this->info = curl_getinfo($this->handle); $options['hooks']->dispatch('curl.after_request', array(&$this->headers)); return $this->headers; @@ -371,6 +425,7 @@ public function stream_headers($handle, $headers) { * @return integer Length of provided data */ protected function stream_body($handle, $data) { + $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit)); $data_length = strlen($data); // Are we limiting the response size? @@ -435,14 +490,16 @@ protected static function format_get($url, $data) { * @return boolean True if the transport is valid, false otherwise. */ public static function test($capabilities = array()) { - if (!function_exists('curl_init') && !function_exists('curl_exec')) + if (!function_exists('curl_init') && !function_exists('curl_exec')) { return false; + } // If needed, check that our installed curl version supports SSL - if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) { + if (isset($capabilities['ssl']) && $capabilities['ssl']) { $curl_version = curl_version(); - if (!(CURL_VERSION_SSL & $curl_version['features'])) + if (!(CURL_VERSION_SSL & $curl_version['features'])) { return false; + } } return true; diff --git a/includes/Requests/Requests/Transport/fsockopen.php b/includes/Requests/Requests/Transport/fsockopen.php index d6391e9..197d513 100644 --- a/includes/Requests/Requests/Transport/fsockopen.php +++ b/includes/Requests/Requests/Transport/fsockopen.php @@ -59,9 +59,13 @@ public function request($url, $headers = array(), $data = array(), $options = ar $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); + if (empty($url_parts)) { + throw new Requests_Exception('Invalid URL.', 'invalidurl', $url); + } $host = $url_parts['host']; $context = stream_context_create(); $verifyname = false; + $case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers); // HTTPS support if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { @@ -86,7 +90,8 @@ public function request($url, $headers = array(), $data = array(), $options = ar if (isset($options['verify'])) { if ($options['verify'] === false) { $context_options['verify_peer'] = false; - } elseif (is_string($options['verify'])) { + } + elseif (is_string($options['verify'])) { $context_options['cafile'] = $options['verify']; } } @@ -103,9 +108,6 @@ public function request($url, $headers = array(), $data = array(), $options = ar $this->max_bytes = $options['max_bytes']; - $proxy = isset( $options['proxy'] ); - $proxy_auth = $proxy && isset( $options['proxy_username'] ) && isset( $options['proxy_password'] ); - if (!isset($url_parts['port'])) { $url_parts['port'] = 80; } @@ -115,78 +117,73 @@ public function request($url, $headers = array(), $data = array(), $options = ar $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket)); - $fp = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); + $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); restore_error_handler(); - if ($verifyname) { - if (!$this->verify_certificate_from_context($host, $context)) { - throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); - } + if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { + throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } - if (!$fp) { + if (!$socket) { if ($errno === 0) { // Connection issue throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } - else { - throw new Requests_Exception($errstr, 'fsockopenerror'); - return; - } + + throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno); + } + + $data_format = $options['data_format']; + + if ($data_format === 'query') { + $path = self::format_get($url_parts, $data); + $data = ''; } + else { + $path = self::format_get($url_parts, array()); + } + + $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); $request_body = ''; - $out = ''; - switch ($options['type']) { - case Requests::POST: - case Requests::PUT: - case Requests::PATCH: - if (isset($url_parts['path'])) { - $path = $url_parts['path']; - if (isset($url_parts['query'])) { - $path .= '?' . $url_parts['query']; - } - } - else { - $path = '/'; - } + $out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']); - $options['hooks']->dispatch( 'fsockopen.remote_host_path', array( &$path, $url ) ); - $out = $options['type'] . " $path HTTP/1.0\r\n"; + if ($options['type'] !== Requests::TRACE) { + if (is_array($data)) { + $request_body = http_build_query($data, null, '&'); + } + else { + $request_body = $data; + } - if (is_array($data)) { - $request_body = http_build_query($data, null, '&'); - } - else { - $request_body = $data; - } - if (empty($headers['Content-Length'])) { + if (!empty($data)) { + if (!isset($case_insensitive_headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } - if (empty($headers['Content-Type'])) { + + if (!isset($case_insensitive_headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } - break; - case Requests::HEAD: - case Requests::GET: - case Requests::DELETE: - $path = self::format_get($url_parts, $data); - $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); - $out = $options['type'] . " $path HTTP/1.0\r\n"; - break; + } + } + + if (!isset($case_insensitive_headers['Host'])) { + $out .= sprintf('Host: %s', $url_parts['host']); + + if ($url_parts['port'] !== 80) { + $out .= ':' . $url_parts['port']; + } + $out .= "\r\n"; } - $out .= "Host: {$url_parts['host']}"; - if ($url_parts['port'] !== 80) { - $out .= ":{$url_parts['port']}"; + if (!isset($case_insensitive_headers['User-Agent'])) { + $out .= sprintf("User-Agent: %s\r\n", $options['useragent']); } - $out .= "\r\n"; - $out .= "User-Agent: {$options['useragent']}\r\n"; $accept_encoding = $this->accept_encoding(); - if (!empty($accept_encoding)) { - $out .= "Accept-Encoding: $accept_encoding\r\n"; + if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { + $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); } $headers = Requests::flatten($headers); @@ -201,28 +198,35 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out .= "\r\n"; } - $out .= "Connection: Close\r\n\r\n" . $request_body; + if (!isset($case_insensitive_headers['Connection'])) { + $out .= "Connection: Close\r\n"; + } + + $out .= "\r\n" . $request_body; $options['hooks']->dispatch('fsockopen.before_send', array(&$out)); - fwrite($fp, $out); - $options['hooks']->dispatch('fsockopen.after_send', array(&$fake_headers)); + fwrite($socket, $out); + $options['hooks']->dispatch('fsockopen.after_send', array($out)); if (!$options['blocking']) { - fclose($fp); + fclose($socket); $fake_headers = ''; $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers)); return ''; } $timeout_sec = (int) floor($options['timeout']); - $timeout_msec = $timeout_sec == $options['timeout'] ? 0 : self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; - stream_set_timeout($fp, $timeout_sec, $timeout_msec); - - $this->info = stream_get_meta_data($fp); + if ($timeout_sec == $options['timeout']) { + $timeout_msec = 0; + } + else { + $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; + } + stream_set_timeout($socket, $timeout_sec, $timeout_msec); $response = $body = $headers = ''; - $this->info = stream_get_meta_data($fp); + $this->info = stream_get_meta_data($socket); $size = 0; $doingbody = false; $download = false; @@ -230,13 +234,13 @@ public function request($url, $headers = array(), $data = array(), $options = ar $download = fopen($options['filename'], 'wb'); } - while (!feof($fp)) { - $this->info = stream_get_meta_data($fp); + while (!feof($socket)) { + $this->info = stream_get_meta_data($socket); if ($this->info['timed_out']) { throw new Requests_Exception('fsocket timed out', 'timeout'); } - $block = fread($fp, Requests::BUFFER_SIZE); + $block = fread($socket, Requests::BUFFER_SIZE); if (!$doingbody) { $response .= $block; if (strpos($response, "\r\n\r\n")) { @@ -247,6 +251,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar // Are we in body mode now? if ($doingbody) { + $options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes)); $data_length = strlen($block); if ($this->max_bytes) { // Have we already hit a limit? @@ -277,7 +282,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar else { $this->headers .= "\r\n\r\n" . $body; } - fclose($fp); + fclose($socket); $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers)); return $this->headers; @@ -341,8 +346,9 @@ protected static function accept_encoding() { */ protected static function format_get($url_parts, $data) { if (!empty($data)) { - if (empty($url_parts['query'])) + if (empty($url_parts['query'])) { $url_parts['query'] = ''; + } $url_parts['query'] .= '&' . http_build_query($data, null, '&'); $url_parts['query'] = trim($url_parts['query'], '&'); @@ -414,17 +420,20 @@ public function verify_certificate_from_context($host, $context) { * @return boolean True if the transport is valid, false otherwise. */ public static function test($capabilities = array()) { - if (!function_exists('fsockopen')) + if (!function_exists('fsockopen')) { return false; + } // If needed, check that streams support SSL - if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) { - if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) + if (isset($capabilities['ssl']) && $capabilities['ssl']) { + if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { return false; + } // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156 - if (defined('HHVM_VERSION')) + if (defined('HHVM_VERSION')) { return false; + } } return true; diff --git a/includes/Requests/Requests/Utility/CaseInsensitiveDictionary.php b/includes/Requests/Requests/Utility/CaseInsensitiveDictionary.php index f6e1496..2c97893 100644 --- a/includes/Requests/Requests/Utility/CaseInsensitiveDictionary.php +++ b/includes/Requests/Requests/Utility/CaseInsensitiveDictionary.php @@ -20,6 +20,17 @@ class Requests_Utility_CaseInsensitiveDictionary implements ArrayAccess, Iterato */ protected $data = array(); + /** + * Creates a case insensitive dictionary. + * + * @param array $data Dictionary/map to convert to case-insensitive + */ + public function __construct(array $data = array()) { + foreach ($data as $key => $value) { + $this->offsetSet($key, $value); + } + } + /** * Check if the given item exists * @@ -39,8 +50,9 @@ public function offsetExists($key) { */ public function offsetGet($key) { $key = strtolower($key); - if (!isset($this->data[$key])) + if (!isset($this->data[$key])) { return null; + } return $this->data[$key]; } diff --git a/includes/Requests/Requests/Utility/FilteredIterator.php b/includes/Requests/Requests/Utility/FilteredIterator.php index 41e2a3d..76a29e7 100644 --- a/includes/Requests/Requests/Utility/FilteredIterator.php +++ b/includes/Requests/Requests/Utility/FilteredIterator.php @@ -13,6 +13,13 @@ * @subpackage Utilities */ class Requests_Utility_FilteredIterator extends ArrayIterator { + /** + * Callback to run as a filter + * + * @var callable + */ + protected $callback; + /** * Create a new iterator * -- 2.45.0