diff --git a/bundled-libs/HTTP/Request2.php b/bundled-libs/HTTP/Request2.php index 36c413fa..b835822e 100644 --- a/bundled-libs/HTTP/Request2.php +++ b/bundled-libs/HTTP/Request2.php @@ -1,1030 +1,1037 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * A class representing an URL as per RFC 3986. - */ -require_once 'Net/URL2.php'; - -/** - * Exception class for HTTP_Request2 package - */ -require_once 'HTTP/Request2/Exception.php'; - -/** - * Class representing a HTTP request message - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://tools.ietf.org/html/rfc2616#section-5 - */ -class HTTP_Request2 implements SplSubject -{ - /**#@+ - * Constants for HTTP request methods - * - * @link http://tools.ietf.org/html/rfc2616#section-5.1.1 - */ - const METHOD_OPTIONS = 'OPTIONS'; - const METHOD_GET = 'GET'; - const METHOD_HEAD = 'HEAD'; - const METHOD_POST = 'POST'; - const METHOD_PUT = 'PUT'; - const METHOD_DELETE = 'DELETE'; - const METHOD_TRACE = 'TRACE'; - const METHOD_CONNECT = 'CONNECT'; - /**#@-*/ - - /**#@+ - * Constants for HTTP authentication schemes - * - * @link http://tools.ietf.org/html/rfc2617 - */ - const AUTH_BASIC = 'basic'; - const AUTH_DIGEST = 'digest'; - /**#@-*/ - - /** - * Regular expression used to check for invalid symbols in RFC 2616 tokens - * @link http://pear.php.net/bugs/bug.php?id=15630 - */ - const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!'; - - /** - * Regular expression used to check for invalid symbols in cookie strings - * @link http://pear.php.net/bugs/bug.php?id=15630 - * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html - */ - const REGEXP_INVALID_COOKIE = '/[\s,;]/'; - - /** - * Fileinfo magic database resource - * @var resource - * @see detectMimeType() - */ - private static $_fileinfoDb; - - /** - * Observers attached to the request (instances of SplObserver) - * @var array - */ - protected $observers = array(); - - /** - * Request URL - * @var Net_URL2 - */ - protected $url; - - /** - * Request method - * @var string - */ - protected $method = self::METHOD_GET; - - /** - * Authentication data - * @var array - * @see getAuth() - */ - protected $auth; - - /** - * Request headers - * @var array - */ - protected $headers = array(); - - /** - * Configuration parameters - * @var array - * @see setConfig() - */ - protected $config = array( - 'adapter' => 'HTTP_Request2_Adapter_Socket', - 'connect_timeout' => 10, - 'timeout' => 0, - 'use_brackets' => true, - 'protocol_version' => '1.1', - 'buffer_size' => 16384, - 'store_body' => true, - 'local_ip' => null, - - 'proxy_host' => '', - 'proxy_port' => '', - 'proxy_user' => '', - 'proxy_password' => '', - 'proxy_auth_scheme' => self::AUTH_BASIC, - 'proxy_type' => 'http', - - 'ssl_verify_peer' => true, - 'ssl_verify_host' => true, - 'ssl_cafile' => null, - 'ssl_capath' => null, - 'ssl_local_cert' => null, - 'ssl_passphrase' => null, - - 'digest_compat_ie' => false, - - 'follow_redirects' => false, - 'max_redirects' => 5, - 'strict_redirects' => false - ); - - /** - * Last event in request / response handling, intended for observers - * @var array - * @see getLastEvent() - */ - protected $lastEvent = array( - 'name' => 'start', - 'data' => null - ); - - /** - * Request body - * @var string|resource - * @see setBody() - */ - protected $body = ''; - - /** - * Array of POST parameters - * @var array - */ - protected $postParams = array(); - - /** - * Array of file uploads (for multipart/form-data POST requests) - * @var array - */ - protected $uploads = array(); - - /** - * Adapter used to perform actual HTTP request - * @var HTTP_Request2_Adapter - */ - protected $adapter; - - /** - * Cookie jar to persist cookies between requests - * @var HTTP_Request2_CookieJar - */ - protected $cookieJar = null; - - /** - * Constructor. Can set request URL, method and configuration array. - * - * Also sets a default value for User-Agent header. - * - * @param string|Net_Url2 $url Request URL - * @param string $method Request method - * @param array $config Configuration for this Request instance - */ - public function __construct( - $url = null, $method = self::METHOD_GET, array $config = array() - ) { - $this->setConfig($config); - if (!empty($url)) { - $this->setUrl($url); - } - if (!empty($method)) { - $this->setMethod($method); - } - $this->setHeader( - 'user-agent', 'HTTP_Request2/2.2.1 ' . - '(http://pear.php.net/package/http_request2) PHP/' . phpversion() - ); - } - - /** - * Sets the URL for this request - * - * If the URL has userinfo part (username & password) these will be removed - * and converted to auth data. If the URL does not have a path component, - * that will be set to '/'. - * - * @param string|Net_URL2 $url Request URL - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function setUrl($url) - { - if (is_string($url)) { - $url = new Net_URL2( - $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets']) - ); - } - if (!$url instanceof Net_URL2) { - throw new HTTP_Request2_LogicException( - 'Parameter is not a valid HTTP URL', - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - // URL contains username / password? - if ($url->getUserinfo()) { - $username = $url->getUser(); - $password = $url->getPassword(); - $this->setAuth(rawurldecode($username), $password? rawurldecode($password): ''); - $url->setUserinfo(''); - } - if ('' == $url->getPath()) { - $url->setPath('/'); - } - $this->url = $url; - - return $this; - } - - /** - * Returns the request URL - * - * @return Net_URL2 - */ - public function getUrl() - { - return $this->url; - } - - /** - * Sets the request method - * - * @param string $method one of the methods defined in RFC 2616 - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException if the method name is invalid - */ - public function setMethod($method) - { - // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1 - if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) { - throw new HTTP_Request2_LogicException( - "Invalid request method '{$method}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $this->method = $method; - - return $this; - } - - /** - * Returns the request method - * - * @return string - */ - public function getMethod() - { - return $this->method; - } - - /** - * Sets the configuration parameter(s) - * - * The following parameters are available: - * - * - * @param string|array $nameOrConfig configuration parameter name or array - * ('parameter name' => 'parameter value') - * @param mixed $value parameter value if $nameOrConfig is not an array - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException If the parameter is unknown - */ - public function setConfig($nameOrConfig, $value = null) - { - if (is_array($nameOrConfig)) { - foreach ($nameOrConfig as $name => $value) { - $this->setConfig($name, $value); - } - - } elseif ('proxy' == $nameOrConfig) { - $url = new Net_URL2($value); - $this->setConfig(array( - 'proxy_type' => $url->getScheme(), - 'proxy_host' => $url->getHost(), - 'proxy_port' => $url->getPort(), - 'proxy_user' => rawurldecode($url->getUser()), - 'proxy_password' => rawurldecode($url->getPassword()) - )); - - } else { - if (!array_key_exists($nameOrConfig, $this->config)) { - throw new HTTP_Request2_LogicException( - "Unknown configuration parameter '{$nameOrConfig}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $this->config[$nameOrConfig] = $value; - } - - return $this; - } - - /** - * Returns the value(s) of the configuration parameter(s) - * - * @param string $name parameter name - * - * @return mixed value of $name parameter, array of all configuration - * parameters if $name is not given - * @throws HTTP_Request2_LogicException If the parameter is unknown - */ - public function getConfig($name = null) - { - if (null === $name) { - return $this->config; - } elseif (!array_key_exists($name, $this->config)) { - throw new HTTP_Request2_LogicException( - "Unknown configuration parameter '{$name}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - return $this->config[$name]; - } - - /** - * Sets the autentification data - * - * @param string $user user name - * @param string $password password - * @param string $scheme authentication scheme - * - * @return HTTP_Request2 - */ - public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC) - { - if (empty($user)) { - $this->auth = null; - } else { - $this->auth = array( - 'user' => (string)$user, - 'password' => (string)$password, - 'scheme' => $scheme - ); - } - - return $this; - } - - /** - * Returns the authentication data - * - * The array has the keys 'user', 'password' and 'scheme', where 'scheme' - * is one of the HTTP_Request2::AUTH_* constants. - * - * @return array - */ - public function getAuth() - { - return $this->auth; - } - - /** - * Sets request header(s) - * - * The first parameter may be either a full header string 'header: value' or - * header name. In the former case $value parameter is ignored, in the latter - * the header's value will either be set to $value or the header will be - * removed if $value is null. The first parameter can also be an array of - * headers, in that case method will be called recursively. - * - * Note that headers are treated case insensitively as per RFC 2616. - * - * - * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar' - * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz' - * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux' - * $req->setHeader('FOO'); // removes 'Foo' header from request - * - * - * @param string|array $name header name, header string ('Header: value') - * or an array of headers - * @param string|array|null $value header value if $name is not an array, - * header will be removed if value is null - * @param bool $replace whether to replace previous header with the - * same name or append to its value - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function setHeader($name, $value = null, $replace = true) - { - if (is_array($name)) { - foreach ($name as $k => $v) { - if (is_string($k)) { - $this->setHeader($k, $v, $replace); - } else { - $this->setHeader($v, null, $replace); - } - } - } else { - if (null === $value && strpos($name, ':')) { - list($name, $value) = array_map('trim', explode(':', $name, 2)); - } - // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2 - if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) { - throw new HTTP_Request2_LogicException( - "Invalid header name '{$name}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - // Header names are case insensitive anyway - $name = strtolower($name); - if (null === $value) { - unset($this->headers[$name]); - - } else { - if (is_array($value)) { - $value = implode(', ', array_map('trim', $value)); - } elseif (is_string($value)) { - $value = trim($value); - } - if (!isset($this->headers[$name]) || $replace) { - $this->headers[$name] = $value; - } else { - $this->headers[$name] .= ', ' . $value; - } - } - } - - return $this; - } - - /** - * Returns the request headers - * - * The array is of the form ('header name' => 'header value'), header names - * are lowercased - * - * @return array - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * Adds a cookie to the request - * - * If the request does not have a CookieJar object set, this method simply - * appends a cookie to "Cookie:" header. - * - * If a CookieJar object is available, the cookie is stored in that object. - * Data from request URL will be used for setting its 'domain' and 'path' - * parameters, 'expires' and 'secure' will be set to null and false, - * respectively. If you need further control, use CookieJar's methods. - * - * @param string $name cookie name - * @param string $value cookie value - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - * @see setCookieJar() - */ - public function addCookie($name, $value) - { - if (!empty($this->cookieJar)) { - $this->cookieJar->store( - array('name' => $name, 'value' => $value), $this->url - ); - - } else { - $cookie = $name . '=' . $value; - if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) { - throw new HTTP_Request2_LogicException( - "Invalid cookie: '{$cookie}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; '; - $this->setHeader('cookie', $cookies . $cookie); - } - - return $this; - } - - /** - * Sets the request body - * - * If you provide file pointer rather than file name, it should support - * fstat() and rewind() operations. - * - * @param string|resource|HTTP_Request2_MultipartBody $body Either a - * string with the body or filename containing body or - * pointer to an open file or object with multipart body data - * @param bool $isFilename Whether - * first parameter is a filename - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function setBody($body, $isFilename = false) - { - if (!$isFilename && !is_resource($body)) { - if (!$body instanceof HTTP_Request2_MultipartBody) { - $this->body = (string)$body; - } else { - $this->body = $body; - } - } else { - $fileData = $this->fopenWrapper($body, empty($this->headers['content-type'])); - $this->body = $fileData['fp']; - if (empty($this->headers['content-type'])) { - $this->setHeader('content-type', $fileData['type']); - } - } - $this->postParams = $this->uploads = array(); - - return $this; - } - - /** - * Returns the request body - * - * @return string|resource|HTTP_Request2_MultipartBody - */ - public function getBody() - { - if (self::METHOD_POST == $this->method - && (!empty($this->postParams) || !empty($this->uploads)) - ) { - if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) { - $body = http_build_query($this->postParams, '', '&'); - if (!$this->getConfig('use_brackets')) { - $body = preg_replace('/%5B\d+%5D=/', '=', $body); - } - // support RFC 3986 by not encoding '~' symbol (request #15368) - return str_replace('%7E', '~', $body); - - } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) { - require_once 'HTTP/Request2/MultipartBody.php'; - return new HTTP_Request2_MultipartBody( - $this->postParams, $this->uploads, $this->getConfig('use_brackets') - ); - } - } - return $this->body; - } - - /** - * Adds a file to form-based file upload - * - * Used to emulate file upload via a HTML form. The method also sets - * Content-Type of HTTP request to 'multipart/form-data'. - * - * If you just want to send the contents of a file as the body of HTTP - * request you should use setBody() method. - * - * If you provide file pointers rather than file names, they should support - * fstat() and rewind() operations. - * - * @param string $fieldName name of file-upload field - * @param string|resource|array $filename full name of local file, - * pointer to open file or an array of files - * @param string $sendFilename filename to send in the request - * @param string $contentType content-type of file being uploaded - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function addUpload( - $fieldName, $filename, $sendFilename = null, $contentType = null - ) { - if (!is_array($filename)) { - $fileData = $this->fopenWrapper($filename, empty($contentType)); - $this->uploads[$fieldName] = array( - 'fp' => $fileData['fp'], - 'filename' => !empty($sendFilename)? $sendFilename - :(is_string($filename)? basename($filename): 'anonymous.blob') , - 'size' => $fileData['size'], - 'type' => empty($contentType)? $fileData['type']: $contentType - ); - } else { - $fps = $names = $sizes = $types = array(); - foreach ($filename as $f) { - if (!is_array($f)) { - $f = array($f); - } - $fileData = $this->fopenWrapper($f[0], empty($f[2])); - $fps[] = $fileData['fp']; - $names[] = !empty($f[1])? $f[1] - :(is_string($f[0])? basename($f[0]): 'anonymous.blob'); - $sizes[] = $fileData['size']; - $types[] = empty($f[2])? $fileData['type']: $f[2]; - } - $this->uploads[$fieldName] = array( - 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types - ); - } - if (empty($this->headers['content-type']) - || 'application/x-www-form-urlencoded' == $this->headers['content-type'] - ) { - $this->setHeader('content-type', 'multipart/form-data'); - } - - return $this; - } - - /** - * Adds POST parameter(s) to the request. - * - * @param string|array $name parameter name or array ('name' => 'value') - * @param mixed $value parameter value (can be an array) - * - * @return HTTP_Request2 - */ - public function addPostParameter($name, $value = null) - { - if (!is_array($name)) { - $this->postParams[$name] = $value; - } else { - foreach ($name as $k => $v) { - $this->addPostParameter($k, $v); - } - } - if (empty($this->headers['content-type'])) { - $this->setHeader('content-type', 'application/x-www-form-urlencoded'); - } - - return $this; - } - - /** - * Attaches a new observer - * - * @param SplObserver $observer any object implementing SplObserver - */ - public function attach(SplObserver $observer) - { - foreach ($this->observers as $attached) { - if ($attached === $observer) { - return; - } - } - $this->observers[] = $observer; - } - - /** - * Detaches an existing observer - * - * @param SplObserver $observer any object implementing SplObserver - */ - public function detach(SplObserver $observer) - { - foreach ($this->observers as $key => $attached) { - if ($attached === $observer) { - unset($this->observers[$key]); - return; - } - } - } - - /** - * Notifies all observers - */ - public function notify() - { - foreach ($this->observers as $observer) { - $observer->update($this); - } - } - - /** - * Sets the last event - * - * Adapters should use this method to set the current state of the request - * and notify the observers. - * - * @param string $name event name - * @param mixed $data event data - */ - public function setLastEvent($name, $data = null) - { - $this->lastEvent = array( - 'name' => $name, - 'data' => $data - ); - $this->notify(); - } - - /** - * Returns the last event - * - * Observers should use this method to access the last change in request. - * The following event names are possible: - * - * Different adapters may not send all the event types. Mock adapter does - * not send any events to the observers. - * - * @return array The array has two keys: 'name' and 'data' - */ - public function getLastEvent() - { - return $this->lastEvent; - } - - /** - * Sets the adapter used to actually perform the request - * - * You can pass either an instance of a class implementing HTTP_Request2_Adapter - * or a class name. The method will only try to include a file if the class - * name starts with HTTP_Request2_Adapter_, it will also try to prepend this - * prefix to the class name if it doesn't contain any underscores, so that - * - * $request->setAdapter('curl'); - * - * will work. - * - * @param string|HTTP_Request2_Adapter $adapter Adapter to use - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function setAdapter($adapter) - { - if (is_string($adapter)) { - if (!class_exists($adapter, false)) { - if (false === strpos($adapter, '_')) { - $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter); - } - if (!class_exists($adapter, false) - && preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter) - ) { - include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php'; - } - if (!class_exists($adapter, false)) { - throw new HTTP_Request2_LogicException( - "Class {$adapter} not found", - HTTP_Request2_Exception::MISSING_VALUE - ); - } - } - $adapter = new $adapter; - } - if (!$adapter instanceof HTTP_Request2_Adapter) { - throw new HTTP_Request2_LogicException( - 'Parameter is not a HTTP request adapter', - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $this->adapter = $adapter; - - return $this; - } - - /** - * Sets the cookie jar - * - * A cookie jar is used to maintain cookies across HTTP requests and - * responses. Cookies from jar will be automatically added to the request - * headers based on request URL. - * - * @param HTTP_Request2_CookieJar|bool $jar Existing CookieJar object, true to - * create a new one, false to remove - * - * @return HTTP_Request2 - * @throws HTTP_Request2_LogicException - */ - public function setCookieJar($jar = true) - { - if (!class_exists('HTTP_Request2_CookieJar', false)) { - require_once 'HTTP/Request2/CookieJar.php'; - } - - if ($jar instanceof HTTP_Request2_CookieJar) { - $this->cookieJar = $jar; - } elseif (true === $jar) { - $this->cookieJar = new HTTP_Request2_CookieJar(); - } elseif (!$jar) { - $this->cookieJar = null; - } else { - throw new HTTP_Request2_LogicException( - 'Invalid parameter passed to setCookieJar()', - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - - return $this; - } - - /** - * Returns current CookieJar object or null if none - * - * @return HTTP_Request2_CookieJar|null - */ - public function getCookieJar() - { - return $this->cookieJar; - } - - /** - * Sends the request and returns the response - * - * @throws HTTP_Request2_Exception - * @return HTTP_Request2_Response - */ - public function send() - { - // Sanity check for URL - if (!$this->url instanceof Net_URL2 - || !$this->url->isAbsolute() - || !in_array(strtolower($this->url->getScheme()), array('https', 'http')) - ) { - throw new HTTP_Request2_LogicException( - 'HTTP_Request2 needs an absolute HTTP(S) request URL, ' - . ($this->url instanceof Net_URL2 - ? "'" . $this->url->__toString() . "'" : 'none') - . ' given', - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - if (empty($this->adapter)) { - $this->setAdapter($this->getConfig('adapter')); - } - // magic_quotes_runtime may break file uploads and chunked response - // processing; see bug #4543. Don't use ini_get() here; see bug #16440. - if ($magicQuotes = get_magic_quotes_runtime()) { - set_magic_quotes_runtime(false); - } - // force using single byte encoding if mbstring extension overloads - // strlen() and substr(); see bug #1781, bug #10605 - if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { - $oldEncoding = mb_internal_encoding(); - mb_internal_encoding('8bit'); - } - - try { - $response = $this->adapter->sendRequest($this); - } catch (Exception $e) { - } - // cleanup in either case (poor man's "finally" clause) - if ($magicQuotes) { - set_magic_quotes_runtime(true); - } - if (!empty($oldEncoding)) { - mb_internal_encoding($oldEncoding); - } - // rethrow the exception - if (!empty($e)) { - throw $e; - } - return $response; - } - - /** - * Wrapper around fopen()/fstat() used by setBody() and addUpload() - * - * @param string|resource $file file name or pointer to open file - * @param bool $detectType whether to try autodetecting MIME - * type of file, will only work if $file is a - * filename, not pointer - * - * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type) - * @throws HTTP_Request2_LogicException - */ - protected function fopenWrapper($file, $detectType = false) - { - if (!is_string($file) && !is_resource($file)) { - throw new HTTP_Request2_LogicException( - "Filename or file pointer resource expected", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $fileData = array( - 'fp' => is_string($file)? null: $file, - 'type' => 'application/octet-stream', - 'size' => 0 - ); - if (is_string($file)) { - if (!($fileData['fp'] = @fopen($file, 'rb'))) { - $error = error_get_last(); - throw new HTTP_Request2_LogicException( - $error['message'], HTTP_Request2_Exception::READ_ERROR - ); - } - if ($detectType) { - $fileData['type'] = self::detectMimeType($file); - } - } - if (!($stat = fstat($fileData['fp']))) { - throw new HTTP_Request2_LogicException( - "fstat() call failed", HTTP_Request2_Exception::READ_ERROR - ); - } - $fileData['size'] = $stat['size']; - - return $fileData; - } - - /** - * Tries to detect MIME type of a file - * - * The method will try to use fileinfo extension if it is available, - * deprecated mime_content_type() function in the other case. If neither - * works, default 'application/octet-stream' MIME type is returned - * - * @param string $filename file name - * - * @return string file MIME type - */ - protected static function detectMimeType($filename) - { - // finfo extension from PECL available - if (function_exists('finfo_open')) { - if (!isset(self::$_fileinfoDb)) { - self::$_fileinfoDb = @finfo_open(FILEINFO_MIME); - } - if (self::$_fileinfoDb) { - $info = finfo_file(self::$_fileinfoDb, $filename); - } - } - // (deprecated) mime_content_type function available - if (empty($info) && function_exists('mime_content_type')) { - return mime_content_type($filename); - } - return empty($info)? 'application/octet-stream': $info; - } -} -?> + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * A class representing an URL as per RFC 3986. + */ +if (!class_exists('Net_URL2', true)) { + require_once 'Net/URL2.php'; +} + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Class representing a HTTP request message + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://tools.ietf.org/html/rfc2616#section-5 + */ +class HTTP_Request2 implements SplSubject +{ + /**#@+ + * Constants for HTTP request methods + * + * @link http://tools.ietf.org/html/rfc2616#section-5.1.1 + */ + const METHOD_OPTIONS = 'OPTIONS'; + const METHOD_GET = 'GET'; + const METHOD_HEAD = 'HEAD'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_DELETE = 'DELETE'; + const METHOD_TRACE = 'TRACE'; + const METHOD_CONNECT = 'CONNECT'; + /**#@-*/ + + /**#@+ + * Constants for HTTP authentication schemes + * + * @link http://tools.ietf.org/html/rfc2617 + */ + const AUTH_BASIC = 'basic'; + const AUTH_DIGEST = 'digest'; + /**#@-*/ + + /** + * Regular expression used to check for invalid symbols in RFC 2616 tokens + * @link http://pear.php.net/bugs/bug.php?id=15630 + */ + const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!'; + + /** + * Regular expression used to check for invalid symbols in cookie strings + * @link http://pear.php.net/bugs/bug.php?id=15630 + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + */ + const REGEXP_INVALID_COOKIE = '/[\s,;]/'; + + /** + * Fileinfo magic database resource + * @var resource + * @see detectMimeType() + */ + private static $_fileinfoDb; + + /** + * Observers attached to the request (instances of SplObserver) + * @var array + */ + protected $observers = array(); + + /** + * Request URL + * @var Net_URL2 + */ + protected $url; + + /** + * Request method + * @var string + */ + protected $method = self::METHOD_GET; + + /** + * Authentication data + * @var array + * @see getAuth() + */ + protected $auth; + + /** + * Request headers + * @var array + */ + protected $headers = array(); + + /** + * Configuration parameters + * @var array + * @see setConfig() + */ + protected $config = array( + 'adapter' => 'HTTP_Request2_Adapter_Socket', + 'connect_timeout' => 10, + 'timeout' => 0, + 'use_brackets' => true, + 'protocol_version' => '1.1', + 'buffer_size' => 16384, + 'store_body' => true, + 'local_ip' => null, + + 'proxy_host' => '', + 'proxy_port' => '', + 'proxy_user' => '', + 'proxy_password' => '', + 'proxy_auth_scheme' => self::AUTH_BASIC, + 'proxy_type' => 'http', + + 'ssl_verify_peer' => true, + 'ssl_verify_host' => true, + 'ssl_cafile' => null, + 'ssl_capath' => null, + 'ssl_local_cert' => null, + 'ssl_passphrase' => null, + + 'digest_compat_ie' => false, + + 'follow_redirects' => false, + 'max_redirects' => 5, + 'strict_redirects' => false + ); + + /** + * Last event in request / response handling, intended for observers + * @var array + * @see getLastEvent() + */ + protected $lastEvent = array( + 'name' => 'start', + 'data' => null + ); + + /** + * Request body + * @var string|resource + * @see setBody() + */ + protected $body = ''; + + /** + * Array of POST parameters + * @var array + */ + protected $postParams = array(); + + /** + * Array of file uploads (for multipart/form-data POST requests) + * @var array + */ + protected $uploads = array(); + + /** + * Adapter used to perform actual HTTP request + * @var HTTP_Request2_Adapter + */ + protected $adapter; + + /** + * Cookie jar to persist cookies between requests + * @var HTTP_Request2_CookieJar + */ + protected $cookieJar = null; + + /** + * Constructor. Can set request URL, method and configuration array. + * + * Also sets a default value for User-Agent header. + * + * @param string|Net_Url2 $url Request URL + * @param string $method Request method + * @param array $config Configuration for this Request instance + */ + public function __construct( + $url = null, $method = self::METHOD_GET, array $config = array() + ) { + $this->setConfig($config); + if (!empty($url)) { + $this->setUrl($url); + } + if (!empty($method)) { + $this->setMethod($method); + } + $this->setHeader( + 'user-agent', 'HTTP_Request2/2.3.0 ' . + '(http://pear.php.net/package/http_request2) PHP/' . phpversion() + ); + } + + /** + * Sets the URL for this request + * + * If the URL has userinfo part (username & password) these will be removed + * and converted to auth data. If the URL does not have a path component, + * that will be set to '/'. + * + * @param string|Net_URL2 $url Request URL + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function setUrl($url) + { + if (is_string($url)) { + $url = new Net_URL2( + $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets']) + ); + } + if (!$url instanceof Net_URL2) { + throw new HTTP_Request2_LogicException( + 'Parameter is not a valid HTTP URL', + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + // URL contains username / password? + if ($url->getUserinfo()) { + $username = $url->getUser(); + $password = $url->getPassword(); + $this->setAuth(rawurldecode($username), $password? rawurldecode($password): ''); + $url->setUserinfo(''); + } + if ('' == $url->getPath()) { + $url->setPath('/'); + } + $this->url = $url; + + return $this; + } + + /** + * Returns the request URL + * + * @return Net_URL2 + */ + public function getUrl() + { + return $this->url; + } + + /** + * Sets the request method + * + * @param string $method one of the methods defined in RFC 2616 + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException if the method name is invalid + */ + public function setMethod($method) + { + // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1 + if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) { + throw new HTTP_Request2_LogicException( + "Invalid request method '{$method}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $this->method = $method; + + return $this; + } + + /** + * Returns the request method + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Sets the configuration parameter(s) + * + * The following parameters are available: + *
    + *
  • 'adapter' - adapter to use (string)
  • + *
  • 'connect_timeout' - Connection timeout in seconds (integer)
  • + *
  • 'timeout' - Total number of seconds a request can take. + * Use 0 for no limit, should be greater than + * 'connect_timeout' if set (integer)
  • + *
  • 'use_brackets' - Whether to append [] to array variable names (bool)
  • + *
  • 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)
  • + *
  • 'buffer_size' - Buffer size to use for reading and writing (int)
  • + *
  • 'store_body' - Whether to store response body in response object. + * Set to false if receiving a huge response and + * using an Observer to save it (boolean)
  • + *
  • 'local_ip' - Specifies the IP address that will be used for accessing + * the network (string)
  • + *
  • 'proxy_type' - Proxy type, 'http' or 'socks5' (string)
  • + *
  • 'proxy_host' - Proxy server host (string)
  • + *
  • 'proxy_port' - Proxy server port (integer)
  • + *
  • 'proxy_user' - Proxy auth username (string)
  • + *
  • 'proxy_password' - Proxy auth password (string)
  • + *
  • 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)
  • + *
  • 'proxy' - Shorthand for proxy_* parameters, proxy given as URL, + * e.g. 'socks5://localhost:1080/' (string)
  • + *
  • 'ssl_verify_peer' - Whether to verify peer's SSL certificate (bool)
  • + *
  • 'ssl_verify_host' - Whether to check that Common Name in SSL + * certificate matches host name (bool)
  • + *
  • 'ssl_cafile' - Cerificate Authority file to verify the peer + * with (use with 'ssl_verify_peer') (string)
  • + *
  • 'ssl_capath' - Directory holding multiple Certificate + * Authority files (string)
  • + *
  • 'ssl_local_cert' - Name of a file containing local cerificate (string)
  • + *
  • 'ssl_passphrase' - Passphrase with which local certificate + * was encoded (string)
  • + *
  • 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6 + * in using URL without query string in digest + * authentication (boolean)
  • + *
  • 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)
  • + *
  • 'max_redirects' - Maximum number of redirects to follow (integer)
  • + *
  • 'strict_redirects' - Whether to keep request method on redirects via status 301 and + * 302 (true, needed for compatibility with RFC 2616) + * or switch to GET (false, needed for compatibility with most + * browsers) (boolean)
  • + *
+ * + * @param string|array $nameOrConfig configuration parameter name or array + * ('parameter name' => 'parameter value') + * @param mixed $value parameter value if $nameOrConfig is not an array + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException If the parameter is unknown + */ + public function setConfig($nameOrConfig, $value = null) + { + if (is_array($nameOrConfig)) { + foreach ($nameOrConfig as $name => $value) { + $this->setConfig($name, $value); + } + + } elseif ('proxy' == $nameOrConfig) { + $url = new Net_URL2($value); + $this->setConfig(array( + 'proxy_type' => $url->getScheme(), + 'proxy_host' => $url->getHost(), + 'proxy_port' => $url->getPort(), + 'proxy_user' => rawurldecode($url->getUser()), + 'proxy_password' => rawurldecode($url->getPassword()) + )); + + } else { + if (!array_key_exists($nameOrConfig, $this->config)) { + throw new HTTP_Request2_LogicException( + "Unknown configuration parameter '{$nameOrConfig}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $this->config[$nameOrConfig] = $value; + } + + return $this; + } + + /** + * Returns the value(s) of the configuration parameter(s) + * + * @param string $name parameter name + * + * @return mixed value of $name parameter, array of all configuration + * parameters if $name is not given + * @throws HTTP_Request2_LogicException If the parameter is unknown + */ + public function getConfig($name = null) + { + if (null === $name) { + return $this->config; + } elseif (!array_key_exists($name, $this->config)) { + throw new HTTP_Request2_LogicException( + "Unknown configuration parameter '{$name}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + return $this->config[$name]; + } + + /** + * Sets the autentification data + * + * @param string $user user name + * @param string $password password + * @param string $scheme authentication scheme + * + * @return HTTP_Request2 + */ + public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC) + { + if (empty($user)) { + $this->auth = null; + } else { + $this->auth = array( + 'user' => (string)$user, + 'password' => (string)$password, + 'scheme' => $scheme + ); + } + + return $this; + } + + /** + * Returns the authentication data + * + * The array has the keys 'user', 'password' and 'scheme', where 'scheme' + * is one of the HTTP_Request2::AUTH_* constants. + * + * @return array + */ + public function getAuth() + { + return $this->auth; + } + + /** + * Sets request header(s) + * + * The first parameter may be either a full header string 'header: value' or + * header name. In the former case $value parameter is ignored, in the latter + * the header's value will either be set to $value or the header will be + * removed if $value is null. The first parameter can also be an array of + * headers, in that case method will be called recursively. + * + * Note that headers are treated case insensitively as per RFC 2616. + * + * + * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar' + * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz' + * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux' + * $req->setHeader('FOO'); // removes 'Foo' header from request + * + * + * @param string|array $name header name, header string ('Header: value') + * or an array of headers + * @param string|array|null $value header value if $name is not an array, + * header will be removed if value is null + * @param bool $replace whether to replace previous header with the + * same name or append to its value + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function setHeader($name, $value = null, $replace = true) + { + if (is_array($name)) { + foreach ($name as $k => $v) { + if (is_string($k)) { + $this->setHeader($k, $v, $replace); + } else { + $this->setHeader($v, null, $replace); + } + } + } else { + if (null === $value && strpos($name, ':')) { + list($name, $value) = array_map('trim', explode(':', $name, 2)); + } + // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2 + if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) { + throw new HTTP_Request2_LogicException( + "Invalid header name '{$name}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + // Header names are case insensitive anyway + $name = strtolower($name); + if (null === $value) { + unset($this->headers[$name]); + + } else { + if (is_array($value)) { + $value = implode(', ', array_map('trim', $value)); + } elseif (is_string($value)) { + $value = trim($value); + } + if (!isset($this->headers[$name]) || $replace) { + $this->headers[$name] = $value; + } else { + $this->headers[$name] .= ', ' . $value; + } + } + } + + return $this; + } + + /** + * Returns the request headers + * + * The array is of the form ('header name' => 'header value'), header names + * are lowercased + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Adds a cookie to the request + * + * If the request does not have a CookieJar object set, this method simply + * appends a cookie to "Cookie:" header. + * + * If a CookieJar object is available, the cookie is stored in that object. + * Data from request URL will be used for setting its 'domain' and 'path' + * parameters, 'expires' and 'secure' will be set to null and false, + * respectively. If you need further control, use CookieJar's methods. + * + * @param string $name cookie name + * @param string $value cookie value + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + * @see setCookieJar() + */ + public function addCookie($name, $value) + { + if (!empty($this->cookieJar)) { + $this->cookieJar->store( + array('name' => $name, 'value' => $value), $this->url + ); + + } else { + $cookie = $name . '=' . $value; + if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) { + throw new HTTP_Request2_LogicException( + "Invalid cookie: '{$cookie}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; '; + $this->setHeader('cookie', $cookies . $cookie); + } + + return $this; + } + + /** + * Sets the request body + * + * If you provide file pointer rather than file name, it should support + * fstat() and rewind() operations. + * + * @param string|resource|HTTP_Request2_MultipartBody $body Either a + * string with the body or filename containing body or + * pointer to an open file or object with multipart body data + * @param bool $isFilename Whether + * first parameter is a filename + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function setBody($body, $isFilename = false) + { + if (!$isFilename && !is_resource($body)) { + if (!$body instanceof HTTP_Request2_MultipartBody) { + $this->body = (string)$body; + } else { + $this->body = $body; + } + } else { + $fileData = $this->fopenWrapper($body, empty($this->headers['content-type'])); + $this->body = $fileData['fp']; + if (empty($this->headers['content-type'])) { + $this->setHeader('content-type', $fileData['type']); + } + } + $this->postParams = $this->uploads = array(); + + return $this; + } + + /** + * Returns the request body + * + * @return string|resource|HTTP_Request2_MultipartBody + */ + public function getBody() + { + if (self::METHOD_POST == $this->method + && (!empty($this->postParams) || !empty($this->uploads)) + ) { + if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) { + $body = http_build_query($this->postParams, '', '&'); + if (!$this->getConfig('use_brackets')) { + $body = preg_replace('/%5B\d+%5D=/', '=', $body); + } + // support RFC 3986 by not encoding '~' symbol (request #15368) + return str_replace('%7E', '~', $body); + + } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) { + require_once 'HTTP/Request2/MultipartBody.php'; + return new HTTP_Request2_MultipartBody( + $this->postParams, $this->uploads, $this->getConfig('use_brackets') + ); + } + } + return $this->body; + } + + /** + * Adds a file to form-based file upload + * + * Used to emulate file upload via a HTML form. The method also sets + * Content-Type of HTTP request to 'multipart/form-data'. + * + * If you just want to send the contents of a file as the body of HTTP + * request you should use setBody() method. + * + * If you provide file pointers rather than file names, they should support + * fstat() and rewind() operations. + * + * @param string $fieldName name of file-upload field + * @param string|resource|array $filename full name of local file, + * pointer to open file or an array of files + * @param string $sendFilename filename to send in the request + * @param string $contentType content-type of file being uploaded + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function addUpload( + $fieldName, $filename, $sendFilename = null, $contentType = null + ) { + if (!is_array($filename)) { + $fileData = $this->fopenWrapper($filename, empty($contentType)); + $this->uploads[$fieldName] = array( + 'fp' => $fileData['fp'], + 'filename' => !empty($sendFilename)? $sendFilename + :(is_string($filename)? basename($filename): 'anonymous.blob') , + 'size' => $fileData['size'], + 'type' => empty($contentType)? $fileData['type']: $contentType + ); + } else { + $fps = $names = $sizes = $types = array(); + foreach ($filename as $f) { + if (!is_array($f)) { + $f = array($f); + } + $fileData = $this->fopenWrapper($f[0], empty($f[2])); + $fps[] = $fileData['fp']; + $names[] = !empty($f[1])? $f[1] + :(is_string($f[0])? basename($f[0]): 'anonymous.blob'); + $sizes[] = $fileData['size']; + $types[] = empty($f[2])? $fileData['type']: $f[2]; + } + $this->uploads[$fieldName] = array( + 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types + ); + } + if (empty($this->headers['content-type']) + || 'application/x-www-form-urlencoded' == $this->headers['content-type'] + ) { + $this->setHeader('content-type', 'multipart/form-data'); + } + + return $this; + } + + /** + * Adds POST parameter(s) to the request. + * + * @param string|array $name parameter name or array ('name' => 'value') + * @param mixed $value parameter value (can be an array) + * + * @return HTTP_Request2 + */ + public function addPostParameter($name, $value = null) + { + if (!is_array($name)) { + $this->postParams[$name] = $value; + } else { + foreach ($name as $k => $v) { + $this->addPostParameter($k, $v); + } + } + if (empty($this->headers['content-type'])) { + $this->setHeader('content-type', 'application/x-www-form-urlencoded'); + } + + return $this; + } + + /** + * Attaches a new observer + * + * @param SplObserver $observer any object implementing SplObserver + */ + public function attach(SplObserver $observer) + { + foreach ($this->observers as $attached) { + if ($attached === $observer) { + return; + } + } + $this->observers[] = $observer; + } + + /** + * Detaches an existing observer + * + * @param SplObserver $observer any object implementing SplObserver + */ + public function detach(SplObserver $observer) + { + foreach ($this->observers as $key => $attached) { + if ($attached === $observer) { + unset($this->observers[$key]); + return; + } + } + } + + /** + * Notifies all observers + */ + public function notify() + { + foreach ($this->observers as $observer) { + $observer->update($this); + } + } + + /** + * Sets the last event + * + * Adapters should use this method to set the current state of the request + * and notify the observers. + * + * @param string $name event name + * @param mixed $data event data + */ + public function setLastEvent($name, $data = null) + { + $this->lastEvent = array( + 'name' => $name, + 'data' => $data + ); + $this->notify(); + } + + /** + * Returns the last event + * + * Observers should use this method to access the last change in request. + * The following event names are possible: + *
    + *
  • 'connect' - after connection to remote server, + * data is the destination (string)
  • + *
  • 'disconnect' - after disconnection from server
  • + *
  • 'sentHeaders' - after sending the request headers, + * data is the headers sent (string)
  • + *
  • 'sentBodyPart' - after sending a part of the request body, + * data is the length of that part (int)
  • + *
  • 'sentBody' - after sending the whole request body, + * data is request body length (int)
  • + *
  • 'receivedHeaders' - after receiving the response headers, + * data is HTTP_Request2_Response object
  • + *
  • 'receivedBodyPart' - after receiving a part of the response + * body, data is that part (string)
  • + *
  • 'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still + * encoded by Content-Encoding
  • + *
  • 'receivedBody' - after receiving the complete response + * body, data is HTTP_Request2_Response object
  • + *
  • 'warning' - a problem arose during the request + * that is not severe enough to throw + * an Exception, data is the warning + * message (string). Currently dispatched if + * response body was received incompletely.
  • + *
+ * Different adapters may not send all the event types. Mock adapter does + * not send any events to the observers. + * + * @return array The array has two keys: 'name' and 'data' + */ + public function getLastEvent() + { + return $this->lastEvent; + } + + /** + * Sets the adapter used to actually perform the request + * + * You can pass either an instance of a class implementing HTTP_Request2_Adapter + * or a class name. The method will only try to include a file if the class + * name starts with HTTP_Request2_Adapter_, it will also try to prepend this + * prefix to the class name if it doesn't contain any underscores, so that + * + * $request->setAdapter('curl'); + * + * will work. + * + * @param string|HTTP_Request2_Adapter $adapter Adapter to use + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function setAdapter($adapter) + { + if (is_string($adapter)) { + if (!class_exists($adapter, false)) { + if (false === strpos($adapter, '_')) { + $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter); + } + if (!class_exists($adapter, false) + && preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter) + ) { + include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php'; + } + if (!class_exists($adapter, false)) { + throw new HTTP_Request2_LogicException( + "Class {$adapter} not found", + HTTP_Request2_Exception::MISSING_VALUE + ); + } + } + $adapter = new $adapter; + } + if (!$adapter instanceof HTTP_Request2_Adapter) { + throw new HTTP_Request2_LogicException( + 'Parameter is not a HTTP request adapter', + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $this->adapter = $adapter; + + return $this; + } + + /** + * Sets the cookie jar + * + * A cookie jar is used to maintain cookies across HTTP requests and + * responses. Cookies from jar will be automatically added to the request + * headers based on request URL. + * + * @param HTTP_Request2_CookieJar|bool $jar Existing CookieJar object, true to + * create a new one, false to remove + * + * @return HTTP_Request2 + * @throws HTTP_Request2_LogicException + */ + public function setCookieJar($jar = true) + { + if (!class_exists('HTTP_Request2_CookieJar', false)) { + require_once 'HTTP/Request2/CookieJar.php'; + } + + if ($jar instanceof HTTP_Request2_CookieJar) { + $this->cookieJar = $jar; + } elseif (true === $jar) { + $this->cookieJar = new HTTP_Request2_CookieJar(); + } elseif (!$jar) { + $this->cookieJar = null; + } else { + throw new HTTP_Request2_LogicException( + 'Invalid parameter passed to setCookieJar()', + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + + return $this; + } + + /** + * Returns current CookieJar object or null if none + * + * @return HTTP_Request2_CookieJar|null + */ + public function getCookieJar() + { + return $this->cookieJar; + } + + /** + * Sends the request and returns the response + * + * @throws HTTP_Request2_Exception + * @return HTTP_Request2_Response + */ + public function send() + { + // Sanity check for URL + if (!$this->url instanceof Net_URL2 + || !$this->url->isAbsolute() + || !in_array(strtolower($this->url->getScheme()), array('https', 'http')) + ) { + throw new HTTP_Request2_LogicException( + 'HTTP_Request2 needs an absolute HTTP(S) request URL, ' + . ($this->url instanceof Net_URL2 + ? "'" . $this->url->__toString() . "'" : 'none') + . ' given', + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + if (empty($this->adapter)) { + $this->setAdapter($this->getConfig('adapter')); + } + // magic_quotes_runtime may break file uploads and chunked response + // processing; see bug #4543. Don't use ini_get() here; see bug #16440. + if ($magicQuotes = get_magic_quotes_runtime()) { + set_magic_quotes_runtime(false); + } + // force using single byte encoding if mbstring extension overloads + // strlen() and substr(); see bug #1781, bug #10605 + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + + try { + $response = $this->adapter->sendRequest($this); + } catch (Exception $e) { + } + // cleanup in either case (poor man's "finally" clause) + if ($magicQuotes) { + set_magic_quotes_runtime(true); + } + if (!empty($oldEncoding)) { + mb_internal_encoding($oldEncoding); + } + // rethrow the exception + if (!empty($e)) { + throw $e; + } + return $response; + } + + /** + * Wrapper around fopen()/fstat() used by setBody() and addUpload() + * + * @param string|resource $file file name or pointer to open file + * @param bool $detectType whether to try autodetecting MIME + * type of file, will only work if $file is a + * filename, not pointer + * + * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type) + * @throws HTTP_Request2_LogicException + */ + protected function fopenWrapper($file, $detectType = false) + { + if (!is_string($file) && !is_resource($file)) { + throw new HTTP_Request2_LogicException( + "Filename or file pointer resource expected", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $fileData = array( + 'fp' => is_string($file)? null: $file, + 'type' => 'application/octet-stream', + 'size' => 0 + ); + if (is_string($file)) { + if (!($fileData['fp'] = @fopen($file, 'rb'))) { + $error = error_get_last(); + throw new HTTP_Request2_LogicException( + $error['message'], HTTP_Request2_Exception::READ_ERROR + ); + } + if ($detectType) { + $fileData['type'] = self::detectMimeType($file); + } + } + if (!($stat = fstat($fileData['fp']))) { + throw new HTTP_Request2_LogicException( + "fstat() call failed", HTTP_Request2_Exception::READ_ERROR + ); + } + $fileData['size'] = $stat['size']; + + return $fileData; + } + + /** + * Tries to detect MIME type of a file + * + * The method will try to use fileinfo extension if it is available, + * deprecated mime_content_type() function in the other case. If neither + * works, default 'application/octet-stream' MIME type is returned + * + * @param string $filename file name + * + * @return string file MIME type + */ + protected static function detectMimeType($filename) + { + // finfo extension from PECL available + if (function_exists('finfo_open')) { + if (!isset(self::$_fileinfoDb)) { + self::$_fileinfoDb = @finfo_open(FILEINFO_MIME); + } + if (self::$_fileinfoDb) { + $info = finfo_file(self::$_fileinfoDb, $filename); + } + } + // (deprecated) mime_content_type function available + if (empty($info) && function_exists('mime_content_type')) { + $info = mime_content_type($filename); + } + return empty($info)? 'application/octet-stream': $info; + } +} +?> diff --git a/bundled-libs/HTTP/Request2/Adapter.php b/bundled-libs/HTTP/Request2/Adapter.php index 2b29fcef..03563279 100644 --- a/bundled-libs/HTTP/Request2/Adapter.php +++ b/bundled-libs/HTTP/Request2/Adapter.php @@ -1,137 +1,137 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Class representing a HTTP response - */ -require_once 'HTTP/Request2/Response.php'; - -/** - * Base class for HTTP_Request2 adapters - * - * HTTP_Request2 class itself only defines methods for aggregating the request - * data, all actual work of sending the request to the remote server and - * receiving its response is performed by adapters. - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -abstract class HTTP_Request2_Adapter -{ - /** - * A list of methods that MUST NOT have a request body, per RFC 2616 - * @var array - */ - protected static $bodyDisallowed = array('TRACE'); - - /** - * Methods having defined semantics for request body - * - * Content-Length header (indicating that the body follows, section 4.3 of - * RFC 2616) will be sent for these methods even if no body was added - * - * @var array - * @link http://pear.php.net/bugs/bug.php?id=12900 - * @link http://pear.php.net/bugs/bug.php?id=14740 - */ - protected static $bodyRequired = array('POST', 'PUT'); - - /** - * Request being sent - * @var HTTP_Request2 - */ - protected $request; - - /** - * Request body - * @var string|resource|HTTP_Request2_MultipartBody - * @see HTTP_Request2::getBody() - */ - protected $requestBody; - - /** - * Length of the request body - * @var integer - */ - protected $contentLength; - - /** - * Sends request to the remote server and returns its response - * - * @param HTTP_Request2 $request HTTP request message - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - abstract public function sendRequest(HTTP_Request2 $request); - - /** - * Calculates length of the request body, adds proper headers - * - * @param array &$headers associative array of request headers, this method - * will add proper 'Content-Length' and 'Content-Type' - * headers to this array (or remove them if not needed) - */ - protected function calculateRequestLength(&$headers) - { - $this->requestBody = $this->request->getBody(); - - if (is_string($this->requestBody)) { - $this->contentLength = strlen($this->requestBody); - } elseif (is_resource($this->requestBody)) { - $stat = fstat($this->requestBody); - $this->contentLength = $stat['size']; - rewind($this->requestBody); - } else { - $this->contentLength = $this->requestBody->getLength(); - $headers['content-type'] = 'multipart/form-data; boundary=' . - $this->requestBody->getBoundary(); - $this->requestBody->rewind(); - } - - if (in_array($this->request->getMethod(), self::$bodyDisallowed) - || 0 == $this->contentLength - ) { - // No body: send a Content-Length header nonetheless (request #12900), - // but do that only for methods that require a body (bug #14740) - if (in_array($this->request->getMethod(), self::$bodyRequired)) { - $headers['content-length'] = 0; - } else { - unset($headers['content-length']); - // if the method doesn't require a body and doesn't have a - // body, don't send a Content-Type header. (request #16799) - unset($headers['content-type']); - } - } else { - if (empty($headers['content-type'])) { - $headers['content-type'] = 'application/x-www-form-urlencoded'; - } - // Content-Length should not be sent for chunked Transfer-Encoding (bug #20125) - if (!isset($headers['transfer-encoding'])) { - $headers['content-length'] = $this->contentLength; - } - } - } -} -?> + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Class representing a HTTP response + */ +require_once 'HTTP/Request2/Response.php'; + +/** + * Base class for HTTP_Request2 adapters + * + * HTTP_Request2 class itself only defines methods for aggregating the request + * data, all actual work of sending the request to the remote server and + * receiving its response is performed by adapters. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +abstract class HTTP_Request2_Adapter +{ + /** + * A list of methods that MUST NOT have a request body, per RFC 2616 + * @var array + */ + protected static $bodyDisallowed = array('TRACE'); + + /** + * Methods having defined semantics for request body + * + * Content-Length header (indicating that the body follows, section 4.3 of + * RFC 2616) will be sent for these methods even if no body was added + * + * @var array + * @link http://pear.php.net/bugs/bug.php?id=12900 + * @link http://pear.php.net/bugs/bug.php?id=14740 + */ + protected static $bodyRequired = array('POST', 'PUT'); + + /** + * Request being sent + * @var HTTP_Request2 + */ + protected $request; + + /** + * Request body + * @var string|resource|HTTP_Request2_MultipartBody + * @see HTTP_Request2::getBody() + */ + protected $requestBody; + + /** + * Length of the request body + * @var integer + */ + protected $contentLength; + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + abstract public function sendRequest(HTTP_Request2 $request); + + /** + * Calculates length of the request body, adds proper headers + * + * @param array &$headers associative array of request headers, this method + * will add proper 'Content-Length' and 'Content-Type' + * headers to this array (or remove them if not needed) + */ + protected function calculateRequestLength(&$headers) + { + $this->requestBody = $this->request->getBody(); + + if (is_string($this->requestBody)) { + $this->contentLength = strlen($this->requestBody); + } elseif (is_resource($this->requestBody)) { + $stat = fstat($this->requestBody); + $this->contentLength = $stat['size']; + rewind($this->requestBody); + } else { + $this->contentLength = $this->requestBody->getLength(); + $headers['content-type'] = 'multipart/form-data; boundary=' . + $this->requestBody->getBoundary(); + $this->requestBody->rewind(); + } + + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + || 0 == $this->contentLength + ) { + // No body: send a Content-Length header nonetheless (request #12900), + // but do that only for methods that require a body (bug #14740) + if (in_array($this->request->getMethod(), self::$bodyRequired)) { + $headers['content-length'] = 0; + } else { + unset($headers['content-length']); + // if the method doesn't require a body and doesn't have a + // body, don't send a Content-Type header. (request #16799) + unset($headers['content-type']); + } + } else { + if (empty($headers['content-type'])) { + $headers['content-type'] = 'application/x-www-form-urlencoded'; + } + // Content-Length should not be sent for chunked Transfer-Encoding (bug #20125) + if (!isset($headers['transfer-encoding'])) { + $headers['content-length'] = $this->contentLength; + } + } + } +} +?> diff --git a/bundled-libs/HTTP/Request2/Adapter/Curl.php b/bundled-libs/HTTP/Request2/Adapter/Curl.php index 4fb8eb54..13d4a299 100644 --- a/bundled-libs/HTTP/Request2/Adapter/Curl.php +++ b/bundled-libs/HTTP/Request2/Adapter/Curl.php @@ -1,567 +1,577 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Base class for HTTP_Request2 adapters - */ -require_once 'HTTP/Request2/Adapter.php'; - -/** - * Adapter for HTTP_Request2 wrapping around cURL extension - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter -{ - /** - * Mapping of header names to cURL options - * @var array - */ - protected static $headerMap = array( - 'accept-encoding' => CURLOPT_ENCODING, - 'cookie' => CURLOPT_COOKIE, - 'referer' => CURLOPT_REFERER, - 'user-agent' => CURLOPT_USERAGENT - ); - - /** - * Mapping of SSL context options to cURL options - * @var array - */ - protected static $sslContextMap = array( - 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER, - 'ssl_cafile' => CURLOPT_CAINFO, - 'ssl_capath' => CURLOPT_CAPATH, - 'ssl_local_cert' => CURLOPT_SSLCERT, - 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD - ); - - /** - * Mapping of CURLE_* constants to Exception subclasses and error codes - * @var array - */ - protected static $errorMap = array( - CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException', - HTTP_Request2_Exception::NON_HTTP_REDIRECT), - CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'), - CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'), - CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'), - // error returned from write callback - CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException', - HTTP_Request2_Exception::NON_HTTP_REDIRECT), - CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException', - HTTP_Request2_Exception::TIMEOUT), - CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'), - CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'), - CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException', - HTTP_Request2_Exception::MISCONFIGURATION), - CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException', - HTTP_Request2_Exception::MISCONFIGURATION), - CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException', - HTTP_Request2_Exception::NON_HTTP_REDIRECT), - CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException', - HTTP_Request2_Exception::TOO_MANY_REDIRECTS), - CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'), - CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'), - CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException', - HTTP_Request2_Exception::MISCONFIGURATION), - CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException', - HTTP_Request2_Exception::MISCONFIGURATION), - CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'), - CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'), - CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException', - HTTP_Request2_Exception::INVALID_ARGUMENT), - CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'), - CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'), - CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'), - ); - - /** - * Response being received - * @var HTTP_Request2_Response - */ - protected $response; - - /** - * Whether 'sentHeaders' event was sent to observers - * @var boolean - */ - protected $eventSentHeaders = false; - - /** - * Whether 'receivedHeaders' event was sent to observers - * @var boolean - */ - protected $eventReceivedHeaders = false; - - /** - * Position within request body - * @var integer - * @see callbackReadBody() - */ - protected $position = 0; - - /** - * Information about last transfer, as returned by curl_getinfo() - * @var array - */ - protected $lastInfo; - - /** - * Creates a subclass of HTTP_Request2_Exception from curl error data - * - * @param resource $ch curl handle - * - * @return HTTP_Request2_Exception - */ - protected static function wrapCurlError($ch) - { - $nativeCode = curl_errno($ch); - $message = 'Curl error: ' . curl_error($ch); - if (!isset(self::$errorMap[$nativeCode])) { - return new HTTP_Request2_Exception($message, 0, $nativeCode); - } else { - $class = self::$errorMap[$nativeCode][0]; - $code = empty(self::$errorMap[$nativeCode][1]) - ? 0 : self::$errorMap[$nativeCode][1]; - return new $class($message, $code, $nativeCode); - } - } - - /** - * Sends request to the remote server and returns its response - * - * @param HTTP_Request2 $request HTTP request message - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - public function sendRequest(HTTP_Request2 $request) - { - if (!extension_loaded('curl')) { - throw new HTTP_Request2_LogicException( - 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION - ); - } - - $this->request = $request; - $this->response = null; - $this->position = 0; - $this->eventSentHeaders = false; - $this->eventReceivedHeaders = false; - - try { - if (false === curl_exec($ch = $this->createCurlHandle())) { - $e = self::wrapCurlError($ch); - } - } catch (Exception $e) { - } - if (isset($ch)) { - $this->lastInfo = curl_getinfo($ch); - curl_close($ch); - } - - $response = $this->response; - unset($this->request, $this->requestBody, $this->response); - - if (!empty($e)) { - throw $e; - } - - if ($jar = $request->getCookieJar()) { - $jar->addCookiesFromResponse($response, $request->getUrl()); - } - - if (0 < $this->lastInfo['size_download']) { - $request->setLastEvent('receivedBody', $response); - } - return $response; - } - - /** - * Returns information about last transfer - * - * @return array associative array as returned by curl_getinfo() - */ - public function getInfo() - { - return $this->lastInfo; - } - - /** - * Creates a new cURL handle and populates it with data from the request - * - * @return resource a cURL handle, as created by curl_init() - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_NotImplementedException - */ - protected function createCurlHandle() - { - $ch = curl_init(); - - curl_setopt_array($ch, array( - // setup write callbacks - CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'), - CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'), - // buffer size - CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'), - // connection timeout - CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'), - // save full outgoing headers, in case someone is interested - CURLINFO_HEADER_OUT => true, - // request url - CURLOPT_URL => $this->request->getUrl()->getUrl() - )); - - // set up redirects - if (!$this->request->getConfig('follow_redirects')) { - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); - } else { - if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) { - throw new HTTP_Request2_LogicException( - 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting', - HTTP_Request2_Exception::MISCONFIGURATION - ); - } - curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects')); - // limit redirects to http(s), works in 5.2.10+ - if (defined('CURLOPT_REDIR_PROTOCOLS')) { - curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); - } - // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571 - if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) { - curl_setopt($ch, CURLOPT_POSTREDIR, 3); - } - } - - // set local IP via CURLOPT_INTERFACE (request #19515) - if ($ip = $this->request->getConfig('local_ip')) { - curl_setopt($ch, CURLOPT_INTERFACE, $ip); - } - - // request timeout - if ($timeout = $this->request->getConfig('timeout')) { - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - } - - // set HTTP version - switch ($this->request->getConfig('protocol_version')) { - case '1.0': - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); - break; - case '1.1': - curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - } - - // set request method - switch ($this->request->getMethod()) { - case HTTP_Request2::METHOD_GET: - curl_setopt($ch, CURLOPT_HTTPGET, true); - break; - case HTTP_Request2::METHOD_POST: - curl_setopt($ch, CURLOPT_POST, true); - break; - case HTTP_Request2::METHOD_HEAD: - curl_setopt($ch, CURLOPT_NOBODY, true); - break; - case HTTP_Request2::METHOD_PUT: - curl_setopt($ch, CURLOPT_UPLOAD, true); - break; - default: - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod()); - } - - // set proxy, if needed - if ($host = $this->request->getConfig('proxy_host')) { - if (!($port = $this->request->getConfig('proxy_port'))) { - throw new HTTP_Request2_LogicException( - 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE - ); - } - curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port); - if ($user = $this->request->getConfig('proxy_user')) { - curl_setopt( - $ch, CURLOPT_PROXYUSERPWD, - $user . ':' . $this->request->getConfig('proxy_password') - ); - switch ($this->request->getConfig('proxy_auth_scheme')) { - case HTTP_Request2::AUTH_BASIC: - curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); - break; - case HTTP_Request2::AUTH_DIGEST: - curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST); - } - } - if ($type = $this->request->getConfig('proxy_type')) { - switch ($type) { - case 'http': - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); - break; - case 'socks5': - curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); - break; - default: - throw new HTTP_Request2_NotImplementedException( - "Proxy type '{$type}' is not supported" - ); - } - } - } - - // set authentication data - if ($auth = $this->request->getAuth()) { - curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']); - switch ($auth['scheme']) { - case HTTP_Request2::AUTH_BASIC: - curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - break; - case HTTP_Request2::AUTH_DIGEST: - curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); - } - } - - // set SSL options - foreach ($this->request->getConfig() as $name => $value) { - if ('ssl_verify_host' == $name && null !== $value) { - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0); - } elseif (isset(self::$sslContextMap[$name]) && null !== $value) { - curl_setopt($ch, self::$sslContextMap[$name], $value); - } - } - - $headers = $this->request->getHeaders(); - // make cURL automagically send proper header - if (!isset($headers['accept-encoding'])) { - $headers['accept-encoding'] = ''; - } - - if (($jar = $this->request->getCookieJar()) - && ($cookies = $jar->getMatching($this->request->getUrl(), true)) - ) { - $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; - } - - // set headers having special cURL keys - foreach (self::$headerMap as $name => $option) { - if (isset($headers[$name])) { - curl_setopt($ch, $option, $headers[$name]); - unset($headers[$name]); - } - } - - $this->calculateRequestLength($headers); - if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) { - $this->workaroundPhpBug47204($ch, $headers); - } - - // set headers not having special keys - $headersFmt = array(); - foreach ($headers as $name => $value) { - $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); - $headersFmt[] = $canonicalName . ': ' . $value; - } - curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt); - - return $ch; - } - - /** - * Workaround for PHP bug #47204 that prevents rewinding request body - * - * The workaround consists of reading the entire request body into memory - * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large - * file uploads, use Socket adapter instead. - * - * @param resource $ch cURL handle - * @param array &$headers Request headers - */ - protected function workaroundPhpBug47204($ch, &$headers) - { - // no redirects, no digest auth -> probably no rewind needed - if (!$this->request->getConfig('follow_redirects') - && (!($auth = $this->request->getAuth()) - || HTTP_Request2::AUTH_DIGEST != $auth['scheme']) - ) { - curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody')); - - } else { - // rewind may be needed, read the whole body into memory - if ($this->requestBody instanceof HTTP_Request2_MultipartBody) { - $this->requestBody = $this->requestBody->__toString(); - - } elseif (is_resource($this->requestBody)) { - $fp = $this->requestBody; - $this->requestBody = ''; - while (!feof($fp)) { - $this->requestBody .= fread($fp, 16384); - } - } - // curl hangs up if content-length is present - unset($headers['content-length']); - curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody); - } - } - - /** - * Callback function called by cURL for reading the request body - * - * @param resource $ch cURL handle - * @param resource $fd file descriptor (not used) - * @param integer $length maximum length of data to return - * - * @return string part of the request body, up to $length bytes - */ - protected function callbackReadBody($ch, $fd, $length) - { - if (!$this->eventSentHeaders) { - $this->request->setLastEvent( - 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) - ); - $this->eventSentHeaders = true; - } - if (in_array($this->request->getMethod(), self::$bodyDisallowed) - || 0 == $this->contentLength || $this->position >= $this->contentLength - ) { - return ''; - } - if (is_string($this->requestBody)) { - $string = substr($this->requestBody, $this->position, $length); - } elseif (is_resource($this->requestBody)) { - $string = fread($this->requestBody, $length); - } else { - $string = $this->requestBody->read($length); - } - $this->request->setLastEvent('sentBodyPart', strlen($string)); - $this->position += strlen($string); - return $string; - } - - /** - * Callback function called by cURL for saving the response headers - * - * @param resource $ch cURL handle - * @param string $string response header (with trailing CRLF) - * - * @return integer number of bytes saved - * @see HTTP_Request2_Response::parseHeaderLine() - */ - protected function callbackWriteHeader($ch, $string) - { - // we may receive a second set of headers if doing e.g. digest auth - if ($this->eventReceivedHeaders || !$this->eventSentHeaders) { - // don't bother with 100-Continue responses (bug #15785) - if (!$this->eventSentHeaders - || $this->response->getStatus() >= 200 - ) { - $this->request->setLastEvent( - 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) - ); - } - $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); - // if body wasn't read by a callback, send event with total body size - if ($upload > $this->position) { - $this->request->setLastEvent( - 'sentBodyPart', $upload - $this->position - ); - $this->position = $upload; - } - if ($upload && (!$this->eventSentHeaders - || $this->response->getStatus() >= 200) - ) { - $this->request->setLastEvent('sentBody', $upload); - } - $this->eventSentHeaders = true; - // we'll need a new response object - if ($this->eventReceivedHeaders) { - $this->eventReceivedHeaders = false; - $this->response = null; - } - } - if (empty($this->response)) { - $this->response = new HTTP_Request2_Response( - $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) - ); - } else { - $this->response->parseHeaderLine($string); - if ('' == trim($string)) { - // don't bother with 100-Continue responses (bug #15785) - if (200 <= $this->response->getStatus()) { - $this->request->setLastEvent('receivedHeaders', $this->response); - } - - if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) { - $redirectUrl = new Net_URL2($this->response->getHeader('location')); - - // for versions lower than 5.2.10, check the redirection URL protocol - if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute() - && !in_array($redirectUrl->getScheme(), array('http', 'https')) - ) { - return -1; - } - - if ($jar = $this->request->getCookieJar()) { - $jar->addCookiesFromResponse($this->response, $this->request->getUrl()); - if (!$redirectUrl->isAbsolute()) { - $redirectUrl = $this->request->getUrl()->resolve($redirectUrl); - } - if ($cookies = $jar->getMatching($redirectUrl, true)) { - curl_setopt($ch, CURLOPT_COOKIE, $cookies); - } - } - } - $this->eventReceivedHeaders = true; - } - } - return strlen($string); - } - - /** - * Callback function called by cURL for saving the response body - * - * @param resource $ch cURL handle (not used) - * @param string $string part of the response body - * - * @return integer number of bytes saved - * @throws HTTP_Request2_MessageException - * @see HTTP_Request2_Response::appendBody() - */ - protected function callbackWriteBody($ch, $string) - { - // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if - // response doesn't start with proper HTTP status line (see bug #15716) - if (empty($this->response)) { - throw new HTTP_Request2_MessageException( - "Malformed response: {$string}", - HTTP_Request2_Exception::MALFORMED_RESPONSE - ); - } - if ($this->request->getConfig('store_body')) { - $this->response->appendBody($string); - } - $this->request->setLastEvent('receivedBodyPart', $string); - return strlen($string); - } -} -?> + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Adapter for HTTP_Request2 wrapping around cURL extension + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter +{ + /** + * Mapping of header names to cURL options + * @var array + */ + protected static $headerMap = array( + 'accept-encoding' => CURLOPT_ENCODING, + 'cookie' => CURLOPT_COOKIE, + 'referer' => CURLOPT_REFERER, + 'user-agent' => CURLOPT_USERAGENT + ); + + /** + * Mapping of SSL context options to cURL options + * @var array + */ + protected static $sslContextMap = array( + 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER, + 'ssl_cafile' => CURLOPT_CAINFO, + 'ssl_capath' => CURLOPT_CAPATH, + 'ssl_local_cert' => CURLOPT_SSLCERT, + 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD + ); + + /** + * Mapping of CURLE_* constants to Exception subclasses and error codes + * @var array + */ + protected static $errorMap = array( + CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'), + CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'), + CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'), + // error returned from write callback + CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::TIMEOUT), + CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'), + CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'), + CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::TOO_MANY_REDIRECTS), + CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'), + CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'), + CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'), + CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'), + CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::INVALID_ARGUMENT), + CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'), + CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'), + CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'), + ); + + /** + * Response being received + * @var HTTP_Request2_Response + */ + protected $response; + + /** + * Whether 'sentHeaders' event was sent to observers + * @var boolean + */ + protected $eventSentHeaders = false; + + /** + * Whether 'receivedHeaders' event was sent to observers + * @var boolean + */ + protected $eventReceivedHeaders = false; + + /** + * Whether 'sentBoody' event was sent to observers + * @var boolean + */ + protected $eventSentBody = false; + + /** + * Position within request body + * @var integer + * @see callbackReadBody() + */ + protected $position = 0; + + /** + * Information about last transfer, as returned by curl_getinfo() + * @var array + */ + protected $lastInfo; + + /** + * Creates a subclass of HTTP_Request2_Exception from curl error data + * + * @param resource $ch curl handle + * + * @return HTTP_Request2_Exception + */ + protected static function wrapCurlError($ch) + { + $nativeCode = curl_errno($ch); + $message = 'Curl error: ' . curl_error($ch); + if (!isset(self::$errorMap[$nativeCode])) { + return new HTTP_Request2_Exception($message, 0, $nativeCode); + } else { + $class = self::$errorMap[$nativeCode][0]; + $code = empty(self::$errorMap[$nativeCode][1]) + ? 0 : self::$errorMap[$nativeCode][1]; + return new $class($message, $code, $nativeCode); + } + } + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + if (!extension_loaded('curl')) { + throw new HTTP_Request2_LogicException( + 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION + ); + } + + $this->request = $request; + $this->response = null; + $this->position = 0; + $this->eventSentHeaders = false; + $this->eventReceivedHeaders = false; + $this->eventSentBody = false; + + try { + if (false === curl_exec($ch = $this->createCurlHandle())) { + $e = self::wrapCurlError($ch); + } + } catch (Exception $e) { + } + if (isset($ch)) { + $this->lastInfo = curl_getinfo($ch); + if (CURLE_OK !== curl_errno($ch)) { + $this->request->setLastEvent('warning', curl_error($ch)); + } + curl_close($ch); + } + + $response = $this->response; + unset($this->request, $this->requestBody, $this->response); + + if (!empty($e)) { + throw $e; + } + + if ($jar = $request->getCookieJar()) { + $jar->addCookiesFromResponse($response); + } + + if (0 < $this->lastInfo['size_download']) { + $request->setLastEvent('receivedBody', $response); + } + return $response; + } + + /** + * Returns information about last transfer + * + * @return array associative array as returned by curl_getinfo() + */ + public function getInfo() + { + return $this->lastInfo; + } + + /** + * Creates a new cURL handle and populates it with data from the request + * + * @return resource a cURL handle, as created by curl_init() + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_NotImplementedException + */ + protected function createCurlHandle() + { + $ch = curl_init(); + + curl_setopt_array($ch, array( + // setup write callbacks + CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'), + CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'), + // buffer size + CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'), + // connection timeout + CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'), + // save full outgoing headers, in case someone is interested + CURLINFO_HEADER_OUT => true, + // request url + CURLOPT_URL => $this->request->getUrl()->getUrl() + )); + + // set up redirects + if (!$this->request->getConfig('follow_redirects')) { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + } else { + if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) { + throw new HTTP_Request2_LogicException( + 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects')); + // limit redirects to http(s), works in 5.2.10+ + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571 + if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) { + curl_setopt($ch, CURLOPT_POSTREDIR, 3); + } + } + + // set local IP via CURLOPT_INTERFACE (request #19515) + if ($ip = $this->request->getConfig('local_ip')) { + curl_setopt($ch, CURLOPT_INTERFACE, $ip); + } + + // request timeout + if ($timeout = $this->request->getConfig('timeout')) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + + // set HTTP version + switch ($this->request->getConfig('protocol_version')) { + case '1.0': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + break; + case '1.1': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + } + + // set request method + switch ($this->request->getMethod()) { + case HTTP_Request2::METHOD_GET: + curl_setopt($ch, CURLOPT_HTTPGET, true); + break; + case HTTP_Request2::METHOD_POST: + curl_setopt($ch, CURLOPT_POST, true); + break; + case HTTP_Request2::METHOD_HEAD: + curl_setopt($ch, CURLOPT_NOBODY, true); + break; + case HTTP_Request2::METHOD_PUT: + curl_setopt($ch, CURLOPT_UPLOAD, true); + break; + default: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod()); + } + + // set proxy, if needed + if ($host = $this->request->getConfig('proxy_host')) { + if (!($port = $this->request->getConfig('proxy_port'))) { + throw new HTTP_Request2_LogicException( + 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE + ); + } + curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port); + if ($user = $this->request->getConfig('proxy_user')) { + curl_setopt( + $ch, CURLOPT_PROXYUSERPWD, + $user . ':' . $this->request->getConfig('proxy_password') + ); + switch ($this->request->getConfig('proxy_auth_scheme')) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST); + } + } + if ($type = $this->request->getConfig('proxy_type')) { + switch ($type) { + case 'http': + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + break; + case 'socks5': + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + break; + default: + throw new HTTP_Request2_NotImplementedException( + "Proxy type '{$type}' is not supported" + ); + } + } + } + + // set authentication data + if ($auth = $this->request->getAuth()) { + curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']); + switch ($auth['scheme']) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + } + } + + // set SSL options + foreach ($this->request->getConfig() as $name => $value) { + if ('ssl_verify_host' == $name && null !== $value) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0); + } elseif (isset(self::$sslContextMap[$name]) && null !== $value) { + curl_setopt($ch, self::$sslContextMap[$name], $value); + } + } + + $headers = $this->request->getHeaders(); + // make cURL automagically send proper header + if (!isset($headers['accept-encoding'])) { + $headers['accept-encoding'] = ''; + } + + if (($jar = $this->request->getCookieJar()) + && ($cookies = $jar->getMatching($this->request->getUrl(), true)) + ) { + $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; + } + + // set headers having special cURL keys + foreach (self::$headerMap as $name => $option) { + if (isset($headers[$name])) { + curl_setopt($ch, $option, $headers[$name]); + unset($headers[$name]); + } + } + + $this->calculateRequestLength($headers); + if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) { + $this->workaroundPhpBug47204($ch, $headers); + } + + // set headers not having special keys + $headersFmt = array(); + foreach ($headers as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $headersFmt[] = $canonicalName . ': ' . $value; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt); + + return $ch; + } + + /** + * Workaround for PHP bug #47204 that prevents rewinding request body + * + * The workaround consists of reading the entire request body into memory + * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large + * file uploads, use Socket adapter instead. + * + * @param resource $ch cURL handle + * @param array &$headers Request headers + */ + protected function workaroundPhpBug47204($ch, &$headers) + { + // no redirects, no digest auth -> probably no rewind needed + // also apply workaround only for POSTs, othrerwise we get + // https://pear.php.net/bugs/bug.php?id=20440 for PUTs + if (!$this->request->getConfig('follow_redirects') + && (!($auth = $this->request->getAuth()) + || HTTP_Request2::AUTH_DIGEST != $auth['scheme']) + || HTTP_Request2::METHOD_POST !== $this->request->getMethod() + ) { + curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody')); + + } else { + // rewind may be needed, read the whole body into memory + if ($this->requestBody instanceof HTTP_Request2_MultipartBody) { + $this->requestBody = $this->requestBody->__toString(); + + } elseif (is_resource($this->requestBody)) { + $fp = $this->requestBody; + $this->requestBody = ''; + while (!feof($fp)) { + $this->requestBody .= fread($fp, 16384); + } + } + // curl hangs up if content-length is present + unset($headers['content-length']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody); + } + } + + /** + * Callback function called by cURL for reading the request body + * + * @param resource $ch cURL handle + * @param resource $fd file descriptor (not used) + * @param integer $length maximum length of data to return + * + * @return string part of the request body, up to $length bytes + */ + protected function callbackReadBody($ch, $fd, $length) + { + if (!$this->eventSentHeaders) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + $this->eventSentHeaders = true; + } + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + || 0 == $this->contentLength || $this->position >= $this->contentLength + ) { + return ''; + } + if (is_string($this->requestBody)) { + $string = substr($this->requestBody, $this->position, $length); + } elseif (is_resource($this->requestBody)) { + $string = fread($this->requestBody, $length); + } else { + $string = $this->requestBody->read($length); + } + $this->request->setLastEvent('sentBodyPart', strlen($string)); + $this->position += strlen($string); + return $string; + } + + /** + * Callback function called by cURL for saving the response headers + * + * @param resource $ch cURL handle + * @param string $string response header (with trailing CRLF) + * + * @return integer number of bytes saved + * @see HTTP_Request2_Response::parseHeaderLine() + */ + protected function callbackWriteHeader($ch, $string) + { + if (!$this->eventSentHeaders + // we may receive a second set of headers if doing e.g. digest auth + // but don't bother with 100-Continue responses (bug #15785) + || $this->eventReceivedHeaders && $this->response->getStatus() >= 200 + ) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + } + if (!$this->eventSentBody) { + $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); + // if body wasn't read by the callback, send event with total body size + if ($upload > $this->position) { + $this->request->setLastEvent( + 'sentBodyPart', $upload - $this->position + ); + } + if ($upload > 0) { + $this->request->setLastEvent('sentBody', $upload); + } + } + $this->eventSentHeaders = true; + $this->eventSentBody = true; + + if ($this->eventReceivedHeaders || empty($this->response)) { + $this->eventReceivedHeaders = false; + $this->response = new HTTP_Request2_Response( + $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) + ); + + } else { + $this->response->parseHeaderLine($string); + if ('' == trim($string)) { + // don't bother with 100-Continue responses (bug #15785) + if (200 <= $this->response->getStatus()) { + $this->request->setLastEvent('receivedHeaders', $this->response); + } + + if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) { + $redirectUrl = new Net_URL2($this->response->getHeader('location')); + + // for versions lower than 5.2.10, check the redirection URL protocol + if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute() + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + ) { + return -1; + } + + if ($jar = $this->request->getCookieJar()) { + $jar->addCookiesFromResponse($this->response); + if (!$redirectUrl->isAbsolute()) { + $redirectUrl = $this->request->getUrl()->resolve($redirectUrl); + } + if ($cookies = $jar->getMatching($redirectUrl, true)) { + curl_setopt($ch, CURLOPT_COOKIE, $cookies); + } + } + } + $this->eventReceivedHeaders = true; + $this->eventSentBody = false; + } + } + return strlen($string); + } + + /** + * Callback function called by cURL for saving the response body + * + * @param resource $ch cURL handle (not used) + * @param string $string part of the response body + * + * @return integer number of bytes saved + * @throws HTTP_Request2_MessageException + * @see HTTP_Request2_Response::appendBody() + */ + protected function callbackWriteBody($ch, $string) + { + // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if + // response doesn't start with proper HTTP status line (see bug #15716) + if (empty($this->response)) { + throw new HTTP_Request2_MessageException( + "Malformed response: {$string}", + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } + if ($this->request->getConfig('store_body')) { + $this->response->appendBody($string); + } + $this->request->setLastEvent('receivedBodyPart', $string); + return strlen($string); + } +} +?> diff --git a/bundled-libs/HTTP/Request2/Adapter/Mock.php b/bundled-libs/HTTP/Request2/Adapter/Mock.php index 9659e621..22b42824 100644 --- a/bundled-libs/HTTP/Request2/Adapter/Mock.php +++ b/bundled-libs/HTTP/Request2/Adapter/Mock.php @@ -1,166 +1,166 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Base class for HTTP_Request2 adapters - */ -require_once 'HTTP/Request2/Adapter.php'; - -/** - * Mock adapter intended for testing - * - * Can be used to test applications depending on HTTP_Request2 package without - * actually performing any HTTP requests. This adapter will return responses - * previously added via addResponse() - * - * $mock = new HTTP_Request2_Adapter_Mock(); - * $mock->addResponse("HTTP/1.1 ... "); - * - * $request = new HTTP_Request2(); - * $request->setAdapter($mock); - * - * // This will return the response set above - * $response = $req->send(); - * - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter -{ - /** - * A queue of responses to be returned by sendRequest() - * @var array - */ - protected $responses = array(); - - /** - * Returns the next response from the queue built by addResponse() - * - * Only responses without explicit URLs or with URLs equal to request URL - * will be considered. If matching response is not found or the queue is - * empty then default empty response with status 400 will be returned, - * if an Exception object was added to the queue it will be thrown. - * - * @param HTTP_Request2 $request HTTP request message - * - * @return HTTP_Request2_Response - * @throws Exception - */ - public function sendRequest(HTTP_Request2 $request) - { - $requestUrl = (string)$request->getUrl(); - $response = null; - foreach ($this->responses as $k => $v) { - if (!$v[1] || $requestUrl == $v[1]) { - $response = $v[0]; - array_splice($this->responses, $k, 1); - break; - } - } - if (!$response) { - return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n"); - - } elseif ($response instanceof HTTP_Request2_Response) { - return $response; - - } else { - // rethrow the exception - $class = get_class($response); - $message = $response->getMessage(); - $code = $response->getCode(); - throw new $class($message, $code); - } - } - - /** - * Adds response to the queue - * - * @param mixed $response either a string, a pointer to an open file, - * an instance of HTTP_Request2_Response or Exception - * @param string $url A request URL this response should be valid for - * (see {@link http://pear.php.net/bugs/bug.php?id=19276}) - * - * @throws HTTP_Request2_Exception - */ - public function addResponse($response, $url = null) - { - if (is_string($response)) { - $response = self::createResponseFromString($response); - } elseif (is_resource($response)) { - $response = self::createResponseFromFile($response); - } elseif (!$response instanceof HTTP_Request2_Response && - !$response instanceof Exception - ) { - throw new HTTP_Request2_Exception('Parameter is not a valid response'); - } - $this->responses[] = array($response, $url); - } - - /** - * Creates a new HTTP_Request2_Response object from a string - * - * @param string $str string containing HTTP response message - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - public static function createResponseFromString($str) - { - $parts = preg_split('!(\r?\n){2}!m', $str, 2); - $headerLines = explode("\n", $parts[0]); - $response = new HTTP_Request2_Response(array_shift($headerLines)); - foreach ($headerLines as $headerLine) { - $response->parseHeaderLine($headerLine); - } - $response->parseHeaderLine(''); - if (isset($parts[1])) { - $response->appendBody($parts[1]); - } - return $response; - } - - /** - * Creates a new HTTP_Request2_Response object from a file - * - * @param resource $fp file pointer returned by fopen() - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - public static function createResponseFromFile($fp) - { - $response = new HTTP_Request2_Response(fgets($fp)); - do { - $headerLine = fgets($fp); - $response->parseHeaderLine($headerLine); - } while ('' != trim($headerLine)); - - while (!feof($fp)) { - $response->appendBody(fread($fp, 8192)); - } - return $response; - } -} + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Mock adapter intended for testing + * + * Can be used to test applications depending on HTTP_Request2 package without + * actually performing any HTTP requests. This adapter will return responses + * previously added via addResponse() + * + * $mock = new HTTP_Request2_Adapter_Mock(); + * $mock->addResponse("HTTP/1.1 ... "); + * + * $request = new HTTP_Request2(); + * $request->setAdapter($mock); + * + * // This will return the response set above + * $response = $req->send(); + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter +{ + /** + * A queue of responses to be returned by sendRequest() + * @var array + */ + protected $responses = array(); + + /** + * Returns the next response from the queue built by addResponse() + * + * Only responses without explicit URLs or with URLs equal to request URL + * will be considered. If matching response is not found or the queue is + * empty then default empty response with status 400 will be returned, + * if an Exception object was added to the queue it will be thrown. + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + $requestUrl = (string)$request->getUrl(); + $response = null; + foreach ($this->responses as $k => $v) { + if (!$v[1] || $requestUrl == $v[1]) { + $response = $v[0]; + array_splice($this->responses, $k, 1); + break; + } + } + if (!$response) { + return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n"); + + } elseif ($response instanceof HTTP_Request2_Response) { + return $response; + + } else { + // rethrow the exception + $class = get_class($response); + $message = $response->getMessage(); + $code = $response->getCode(); + throw new $class($message, $code); + } + } + + /** + * Adds response to the queue + * + * @param mixed $response either a string, a pointer to an open file, + * an instance of HTTP_Request2_Response or Exception + * @param string $url A request URL this response should be valid for + * (see {@link http://pear.php.net/bugs/bug.php?id=19276}) + * + * @throws HTTP_Request2_Exception + */ + public function addResponse($response, $url = null) + { + if (is_string($response)) { + $response = self::createResponseFromString($response); + } elseif (is_resource($response)) { + $response = self::createResponseFromFile($response); + } elseif (!$response instanceof HTTP_Request2_Response && + !$response instanceof Exception + ) { + throw new HTTP_Request2_Exception('Parameter is not a valid response'); + } + $this->responses[] = array($response, $url); + } + + /** + * Creates a new HTTP_Request2_Response object from a string + * + * @param string $str string containing HTTP response message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromString($str) + { + $parts = preg_split('!(\r?\n){2}!m', $str, 2); + $headerLines = explode("\n", $parts[0]); + $response = new HTTP_Request2_Response(array_shift($headerLines)); + foreach ($headerLines as $headerLine) { + $response->parseHeaderLine($headerLine); + } + $response->parseHeaderLine(''); + if (isset($parts[1])) { + $response->appendBody($parts[1]); + } + return $response; + } + + /** + * Creates a new HTTP_Request2_Response object from a file + * + * @param resource $fp file pointer returned by fopen() + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromFile($fp) + { + $response = new HTTP_Request2_Response(fgets($fp)); + do { + $headerLine = fgets($fp); + $response->parseHeaderLine($headerLine); + } while ('' != trim($headerLine)); + + while (!feof($fp)) { + $response->appendBody(fread($fp, 8192)); + } + return $response; + } +} ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/Adapter/Socket.php b/bundled-libs/HTTP/Request2/Adapter/Socket.php index 62507fd7..3a1d1860 100644 --- a/bundled-libs/HTTP/Request2/Adapter/Socket.php +++ b/bundled-libs/HTTP/Request2/Adapter/Socket.php @@ -1,1121 +1,1138 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** Base class for HTTP_Request2 adapters */ -require_once 'HTTP/Request2/Adapter.php'; - -/** Socket wrapper class */ -require_once 'HTTP/Request2/SocketWrapper.php'; - -/** - * Socket-based adapter for HTTP_Request2 - * - * This adapter uses only PHP sockets and will work on almost any PHP - * environment. Code is based on original HTTP_Request PEAR package. - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter -{ - /** - * Regular expression for 'token' rule from RFC 2616 - */ - const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+'; - - /** - * Regular expression for 'quoted-string' rule from RFC 2616 - */ - const REGEXP_QUOTED_STRING = '"(?>[^"\\\\]+|\\\\.)*"'; - - /** - * Connected sockets, needed for Keep-Alive support - * @var array - * @see connect() - */ - protected static $sockets = array(); - - /** - * Data for digest authentication scheme - * - * The keys for the array are URL prefixes. - * - * The values are associative arrays with data (realm, nonce, nonce-count, - * opaque...) needed for digest authentication. Stored here to prevent making - * duplicate requests to digest-protected resources after we have already - * received the challenge. - * - * @var array - */ - protected static $challenges = array(); - - /** - * Connected socket - * @var HTTP_Request2_SocketWrapper - * @see connect() - */ - protected $socket; - - /** - * Challenge used for server digest authentication - * @var array - */ - protected $serverChallenge; - - /** - * Challenge used for proxy digest authentication - * @var array - */ - protected $proxyChallenge; - - /** - * Remaining length of the current chunk, when reading chunked response - * @var integer - * @see readChunked() - */ - protected $chunkLength = 0; - - /** - * Remaining amount of redirections to follow - * - * Starts at 'max_redirects' configuration parameter and is reduced on each - * subsequent redirect. An Exception will be thrown once it reaches zero. - * - * @var integer - */ - protected $redirectCountdown = null; - - /** - * Whether to wait for "100 Continue" response before sending request body - * @var bool - */ - protected $expect100Continue = false; - - /** - * Sends request to the remote server and returns its response - * - * @param HTTP_Request2 $request HTTP request message - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - public function sendRequest(HTTP_Request2 $request) - { - $this->request = $request; - - try { - $keepAlive = $this->connect(); - $headers = $this->prepareHeaders(); - $this->socket->write($headers); - // provide request headers to the observer, see request #7633 - $this->request->setLastEvent('sentHeaders', $headers); - - if (!$this->expect100Continue) { - $this->writeBody(); - $response = $this->readResponse(); - - } else { - $response = $this->readResponse(); - if (!$response || 100 == $response->getStatus()) { - $this->expect100Continue = false; - // either got "100 Continue" or timed out -> send body - $this->writeBody(); - $response = $this->readResponse(); - } - } - - - if ($jar = $request->getCookieJar()) { - $jar->addCookiesFromResponse($response, $request->getUrl()); - } - - if (!$this->canKeepAlive($keepAlive, $response)) { - $this->disconnect(); - } - - if ($this->shouldUseProxyDigestAuth($response)) { - return $this->sendRequest($request); - } - if ($this->shouldUseServerDigestAuth($response)) { - return $this->sendRequest($request); - } - if ($authInfo = $response->getHeader('authentication-info')) { - $this->updateChallenge($this->serverChallenge, $authInfo); - } - if ($proxyInfo = $response->getHeader('proxy-authentication-info')) { - $this->updateChallenge($this->proxyChallenge, $proxyInfo); - } - - } catch (Exception $e) { - $this->disconnect(); - } - - unset($this->request, $this->requestBody); - - if (!empty($e)) { - $this->redirectCountdown = null; - throw $e; - } - - if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) { - $this->redirectCountdown = null; - return $response; - } else { - return $this->handleRedirect($request, $response); - } - } - - /** - * Connects to the remote server - * - * @return bool whether the connection can be persistent - * @throws HTTP_Request2_Exception - */ - protected function connect() - { - $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https'); - $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); - $headers = $this->request->getHeaders(); - $reqHost = $this->request->getUrl()->getHost(); - if (!($reqPort = $this->request->getUrl()->getPort())) { - $reqPort = $secure? 443: 80; - } - - $httpProxy = $socksProxy = false; - if (!($host = $this->request->getConfig('proxy_host'))) { - $host = $reqHost; - $port = $reqPort; - } else { - if (!($port = $this->request->getConfig('proxy_port'))) { - throw new HTTP_Request2_LogicException( - 'Proxy port not provided', - HTTP_Request2_Exception::MISSING_VALUE - ); - } - if ('http' == ($type = $this->request->getConfig('proxy_type'))) { - $httpProxy = true; - } elseif ('socks5' == $type) { - $socksProxy = true; - } else { - throw new HTTP_Request2_NotImplementedException( - "Proxy type '{$type}' is not supported" - ); - } - } - - if ($tunnel && !$httpProxy) { - throw new HTTP_Request2_LogicException( - "Trying to perform CONNECT request without proxy", - HTTP_Request2_Exception::MISSING_VALUE - ); - } - if ($secure && !in_array('ssl', stream_get_transports())) { - throw new HTTP_Request2_LogicException( - 'Need OpenSSL support for https:// requests', - HTTP_Request2_Exception::MISCONFIGURATION - ); - } - - // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive - // connection token to a proxy server... - if ($httpProxy && !$secure && !empty($headers['connection']) - && 'Keep-Alive' == $headers['connection'] - ) { - $this->request->setHeader('connection'); - } - - $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && - empty($headers['connection'])) || - (!empty($headers['connection']) && - 'Keep-Alive' == $headers['connection']); - - $options = array(); - if ($ip = $this->request->getConfig('local_ip')) { - $options['socket'] = array( - 'bindto' => (false === strpos($ip, ':') ? $ip : '[' . $ip . ']') . ':0' - ); - } - if ($secure || $tunnel) { - $options['ssl'] = array(); - foreach ($this->request->getConfig() as $name => $value) { - if ('ssl_' == substr($name, 0, 4) && null !== $value) { - if ('ssl_verify_host' == $name) { - if ($value) { - $options['ssl']['CN_match'] = $reqHost; - } - } else { - $options['ssl'][substr($name, 4)] = $value; - } - } - } - ksort($options['ssl']); - } - - // Use global request timeout if given, see feature requests #5735, #8964 - if ($timeout = $this->request->getConfig('timeout')) { - $deadline = time() + $timeout; - } else { - $deadline = null; - } - - // Changing SSL context options after connection is established does *not* - // work, we need a new connection if options change - $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://') - . $host . ':' . $port; - $socketKey = $remote . ( - ($secure && $httpProxy || $socksProxy) - ? "->{$reqHost}:{$reqPort}" : '' - ) . (empty($options)? '': ':' . serialize($options)); - unset($this->socket); - - // We use persistent connections and have a connected socket? - // Ensure that the socket is still connected, see bug #16149 - if ($keepAlive && !empty(self::$sockets[$socketKey]) - && !self::$sockets[$socketKey]->eof() - ) { - $this->socket =& self::$sockets[$socketKey]; - - } else { - if ($socksProxy) { - require_once 'HTTP/Request2/SOCKS5.php'; - - $this->socket = new HTTP_Request2_SOCKS5( - $remote, $this->request->getConfig('connect_timeout'), - $options, $this->request->getConfig('proxy_user'), - $this->request->getConfig('proxy_password') - ); - // handle request timeouts ASAP - $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); - $this->socket->connect($reqHost, $reqPort); - if (!$secure) { - $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}"; - } else { - $this->socket->enableCrypto(); - $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; - } - - } elseif ($secure && $httpProxy && !$tunnel) { - $this->establishTunnel(); - $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; - - } else { - $this->socket = new HTTP_Request2_SocketWrapper( - $remote, $this->request->getConfig('connect_timeout'), $options - ); - } - $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo); - self::$sockets[$socketKey] =& $this->socket; - } - $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); - return $keepAlive; - } - - /** - * Establishes a tunnel to a secure remote server via HTTP CONNECT request - * - * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP - * sees that we are connected to a proxy server (duh!) rather than the server - * that presents its certificate. - * - * @link http://tools.ietf.org/html/rfc2817#section-5.2 - * @throws HTTP_Request2_Exception - */ - protected function establishTunnel() - { - $donor = new self; - $connect = new HTTP_Request2( - $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT, - array_merge($this->request->getConfig(), array('adapter' => $donor)) - ); - $response = $connect->send(); - // Need any successful (2XX) response - if (200 > $response->getStatus() || 300 <= $response->getStatus()) { - throw new HTTP_Request2_ConnectionException( - 'Failed to connect via HTTPS proxy. Proxy response: ' . - $response->getStatus() . ' ' . $response->getReasonPhrase() - ); - } - $this->socket = $donor->socket; - $this->socket->enableCrypto(); - } - - /** - * Checks whether current connection may be reused or should be closed - * - * @param boolean $requestKeepAlive whether connection could - * be persistent in the first place - * @param HTTP_Request2_Response $response response object to check - * - * @return boolean - */ - protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response) - { - // Do not close socket on successful CONNECT request - if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() - && 200 <= $response->getStatus() && 300 > $response->getStatus() - ) { - return true; - } - - $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) - || null !== $response->getHeader('content-length') - // no body possible for such responses, see also request #17031 - || HTTP_Request2::METHOD_HEAD == $this->request->getMethod() - || in_array($response->getStatus(), array(204, 304)); - $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) || - (null === $response->getHeader('connection') && - '1.1' == $response->getVersion()); - return $requestKeepAlive && $lengthKnown && $persistent; - } - - /** - * Disconnects from the remote server - */ - protected function disconnect() - { - if (!empty($this->socket)) { - $this->socket = null; - $this->request->setLastEvent('disconnect'); - } - } - - /** - * Handles HTTP redirection - * - * This method will throw an Exception if redirect to a non-HTTP(S) location - * is attempted, also if number of redirects performed already is equal to - * 'max_redirects' configuration parameter. - * - * @param HTTP_Request2 $request Original request - * @param HTTP_Request2_Response $response Response containing redirect - * - * @return HTTP_Request2_Response Response from a new location - * @throws HTTP_Request2_Exception - */ - protected function handleRedirect( - HTTP_Request2 $request, HTTP_Request2_Response $response - ) { - if (is_null($this->redirectCountdown)) { - $this->redirectCountdown = $request->getConfig('max_redirects'); - } - if (0 == $this->redirectCountdown) { - $this->redirectCountdown = null; - // Copying cURL behaviour - throw new HTTP_Request2_MessageException( - 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed', - HTTP_Request2_Exception::TOO_MANY_REDIRECTS - ); - } - $redirectUrl = new Net_URL2( - $response->getHeader('location'), - array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets')) - ); - // refuse non-HTTP redirect - if ($redirectUrl->isAbsolute() - && !in_array($redirectUrl->getScheme(), array('http', 'https')) - ) { - $this->redirectCountdown = null; - throw new HTTP_Request2_MessageException( - 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(), - HTTP_Request2_Exception::NON_HTTP_REDIRECT - ); - } - // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30), - // but in practice it is often not - if (!$redirectUrl->isAbsolute()) { - $redirectUrl = $request->getUrl()->resolve($redirectUrl); - } - $redirect = clone $request; - $redirect->setUrl($redirectUrl); - if (303 == $response->getStatus() - || (!$request->getConfig('strict_redirects') - && in_array($response->getStatus(), array(301, 302))) - ) { - $redirect->setMethod(HTTP_Request2::METHOD_GET); - $redirect->setBody(''); - } - - if (0 < $this->redirectCountdown) { - $this->redirectCountdown--; - } - return $this->sendRequest($redirect); - } - - /** - * Checks whether another request should be performed with server digest auth - * - * Several conditions should be satisfied for it to return true: - * - response status should be 401 - * - auth credentials should be set in the request object - * - response should contain WWW-Authenticate header with digest challenge - * - there is either no challenge stored for this URL or new challenge - * contains stale=true parameter (in other case we probably just failed - * due to invalid username / password) - * - * The method stores challenge values in $challenges static property - * - * @param HTTP_Request2_Response $response response to check - * - * @return boolean whether another request should be performed - * @throws HTTP_Request2_Exception in case of unsupported challenge parameters - */ - protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response) - { - // no sense repeating a request if we don't have credentials - if (401 != $response->getStatus() || !$this->request->getAuth()) { - return false; - } - if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) { - return false; - } - - $url = $this->request->getUrl(); - $scheme = $url->getScheme(); - $host = $scheme . '://' . $url->getHost(); - if ($port = $url->getPort()) { - if ((0 == strcasecmp($scheme, 'http') && 80 != $port) - || (0 == strcasecmp($scheme, 'https') && 443 != $port) - ) { - $host .= ':' . $port; - } - } - - if (!empty($challenge['domain'])) { - $prefixes = array(); - foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) { - // don't bother with different servers - if ('/' == substr($prefix, 0, 1)) { - $prefixes[] = $host . $prefix; - } - } - } - if (empty($prefixes)) { - $prefixes = array($host . '/'); - } - - $ret = true; - foreach ($prefixes as $prefix) { - if (!empty(self::$challenges[$prefix]) - && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) - ) { - // probably credentials are invalid - $ret = false; - } - self::$challenges[$prefix] =& $challenge; - } - return $ret; - } - - /** - * Checks whether another request should be performed with proxy digest auth - * - * Several conditions should be satisfied for it to return true: - * - response status should be 407 - * - proxy auth credentials should be set in the request object - * - response should contain Proxy-Authenticate header with digest challenge - * - there is either no challenge stored for this proxy or new challenge - * contains stale=true parameter (in other case we probably just failed - * due to invalid username / password) - * - * The method stores challenge values in $challenges static property - * - * @param HTTP_Request2_Response $response response to check - * - * @return boolean whether another request should be performed - * @throws HTTP_Request2_Exception in case of unsupported challenge parameters - */ - protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response) - { - if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) { - return false; - } - if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) { - return false; - } - - $key = 'proxy://' . $this->request->getConfig('proxy_host') . - ':' . $this->request->getConfig('proxy_port'); - - if (!empty(self::$challenges[$key]) - && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) - ) { - $ret = false; - } else { - $ret = true; - } - self::$challenges[$key] = $challenge; - return $ret; - } - - /** - * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value - * - * There is a problem with implementation of RFC 2617: several of the parameters - * are defined as quoted-string there and thus may contain backslash escaped - * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as - * just value of quoted-string X without surrounding quotes, it doesn't speak - * about removing backslash escaping. - * - * Now realm parameter is user-defined and human-readable, strange things - * happen when it contains quotes: - * - Apache allows quotes in realm, but apparently uses realm value without - * backslashes for digest computation - * - Squid allows (manually escaped) quotes there, but it is impossible to - * authorize with either escaped or unescaped quotes used in digest, - * probably it can't parse the response (?) - * - Both IE and Firefox display realm value with backslashes in - * the password popup and apparently use the same value for digest - * - * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in - * quoted-string handling, unfortunately that means failure to authorize - * sometimes - * - * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header - * - * @return mixed associative array with challenge parameters, false if - * no challenge is present in header value - * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters - */ - protected function parseDigestChallenge($headerValue) - { - $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . - self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')'; - $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!"; - if (!preg_match($challenge, $headerValue, $matches)) { - return false; - } - - preg_match_all('!' . $authParam . '!', $matches[0], $params); - $paramsAry = array(); - $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale', - 'algorithm', 'qop'); - for ($i = 0; $i < count($params[0]); $i++) { - // section 3.2.1: Any unrecognized directive MUST be ignored. - if (in_array($params[1][$i], $knownParams)) { - if ('"' == substr($params[2][$i], 0, 1)) { - $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); - } else { - $paramsAry[$params[1][$i]] = $params[2][$i]; - } - } - } - // we only support qop=auth - if (!empty($paramsAry['qop']) - && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))) - ) { - throw new HTTP_Request2_NotImplementedException( - "Only 'auth' qop is currently supported in digest authentication, " . - "server requested '{$paramsAry['qop']}'" - ); - } - // we only support algorithm=MD5 - if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) { - throw new HTTP_Request2_NotImplementedException( - "Only 'MD5' algorithm is currently supported in digest authentication, " . - "server requested '{$paramsAry['algorithm']}'" - ); - } - - return $paramsAry; - } - - /** - * Parses [Proxy-]Authentication-Info header value and updates challenge - * - * @param array &$challenge challenge to update - * @param string $headerValue value of [Proxy-]Authentication-Info header - * - * @todo validate server rspauth response - */ - protected function updateChallenge(&$challenge, $headerValue) - { - $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . - self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!'; - $paramsAry = array(); - - preg_match_all($authParam, $headerValue, $params); - for ($i = 0; $i < count($params[0]); $i++) { - if ('"' == substr($params[2][$i], 0, 1)) { - $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); - } else { - $paramsAry[$params[1][$i]] = $params[2][$i]; - } - } - // for now, just update the nonce value - if (!empty($paramsAry['nextnonce'])) { - $challenge['nonce'] = $paramsAry['nextnonce']; - $challenge['nc'] = 1; - } - } - - /** - * Creates a value for [Proxy-]Authorization header when using digest authentication - * - * @param string $user user name - * @param string $password password - * @param string $url request URL - * @param array &$challenge digest challenge parameters - * - * @return string value of [Proxy-]Authorization request header - * @link http://tools.ietf.org/html/rfc2617#section-3.2.2 - */ - protected function createDigestResponse($user, $password, $url, &$challenge) - { - if (false !== ($q = strpos($url, '?')) - && $this->request->getConfig('digest_compat_ie') - ) { - $url = substr($url, 0, $q); - } - - $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password); - $a2 = md5($this->request->getMethod() . ':' . $url); - - if (empty($challenge['qop'])) { - $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2); - } else { - $challenge['cnonce'] = 'Req2.' . rand(); - if (empty($challenge['nc'])) { - $challenge['nc'] = 1; - } - $nc = sprintf('%08x', $challenge['nc']++); - $digest = md5( - $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . - $challenge['cnonce'] . ':auth:' . $a2 - ); - } - return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' . - 'realm="' . $challenge['realm'] . '", ' . - 'nonce="' . $challenge['nonce'] . '", ' . - 'uri="' . $url . '", ' . - 'response="' . $digest . '"' . - (!empty($challenge['opaque'])? - ', opaque="' . $challenge['opaque'] . '"': - '') . - (!empty($challenge['qop'])? - ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"': - ''); - } - - /** - * Adds 'Authorization' header (if needed) to request headers array - * - * @param array &$headers request headers - * @param string $requestHost request host (needed for digest authentication) - * @param string $requestUrl request URL (needed for digest authentication) - * - * @throws HTTP_Request2_NotImplementedException - */ - protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl) - { - if (!($auth = $this->request->getAuth())) { - return; - } - switch ($auth['scheme']) { - case HTTP_Request2::AUTH_BASIC: - $headers['authorization'] = 'Basic ' . base64_encode( - $auth['user'] . ':' . $auth['password'] - ); - break; - - case HTTP_Request2::AUTH_DIGEST: - unset($this->serverChallenge); - $fullUrl = ('/' == $requestUrl[0])? - $this->request->getUrl()->getScheme() . '://' . - $requestHost . $requestUrl: - $requestUrl; - foreach (array_keys(self::$challenges) as $key) { - if ($key == substr($fullUrl, 0, strlen($key))) { - $headers['authorization'] = $this->createDigestResponse( - $auth['user'], $auth['password'], - $requestUrl, self::$challenges[$key] - ); - $this->serverChallenge =& self::$challenges[$key]; - break; - } - } - break; - - default: - throw new HTTP_Request2_NotImplementedException( - "Unknown HTTP authentication scheme '{$auth['scheme']}'" - ); - } - } - - /** - * Adds 'Proxy-Authorization' header (if needed) to request headers array - * - * @param array &$headers request headers - * @param string $requestUrl request URL (needed for digest authentication) - * - * @throws HTTP_Request2_NotImplementedException - */ - protected function addProxyAuthorizationHeader(&$headers, $requestUrl) - { - if (!$this->request->getConfig('proxy_host') - || !($user = $this->request->getConfig('proxy_user')) - || (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) - && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod()) - ) { - return; - } - - $password = $this->request->getConfig('proxy_password'); - switch ($this->request->getConfig('proxy_auth_scheme')) { - case HTTP_Request2::AUTH_BASIC: - $headers['proxy-authorization'] = 'Basic ' . base64_encode( - $user . ':' . $password - ); - break; - - case HTTP_Request2::AUTH_DIGEST: - unset($this->proxyChallenge); - $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') . - ':' . $this->request->getConfig('proxy_port'); - if (!empty(self::$challenges[$proxyUrl])) { - $headers['proxy-authorization'] = $this->createDigestResponse( - $user, $password, - $requestUrl, self::$challenges[$proxyUrl] - ); - $this->proxyChallenge =& self::$challenges[$proxyUrl]; - } - break; - - default: - throw new HTTP_Request2_NotImplementedException( - "Unknown HTTP authentication scheme '" . - $this->request->getConfig('proxy_auth_scheme') . "'" - ); - } - } - - - /** - * Creates the string with the Request-Line and request headers - * - * @return string - * @throws HTTP_Request2_Exception - */ - protected function prepareHeaders() - { - $headers = $this->request->getHeaders(); - $url = $this->request->getUrl(); - $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); - $host = $url->getHost(); - - $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80; - if (($port = $url->getPort()) && $port != $defaultPort || $connect) { - $host .= ':' . (empty($port)? $defaultPort: $port); - } - // Do not overwrite explicitly set 'Host' header, see bug #16146 - if (!isset($headers['host'])) { - $headers['host'] = $host; - } - - if ($connect) { - $requestUrl = $host; - - } else { - if (!$this->request->getConfig('proxy_host') - || 'http' != $this->request->getConfig('proxy_type') - || 0 == strcasecmp($url->getScheme(), 'https') - ) { - $requestUrl = ''; - } else { - $requestUrl = $url->getScheme() . '://' . $host; - } - $path = $url->getPath(); - $query = $url->getQuery(); - $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query); - } - - if ('1.1' == $this->request->getConfig('protocol_version') - && extension_loaded('zlib') && !isset($headers['accept-encoding']) - ) { - $headers['accept-encoding'] = 'gzip, deflate'; - } - if (($jar = $this->request->getCookieJar()) - && ($cookies = $jar->getMatching($this->request->getUrl(), true)) - ) { - $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; - } - - $this->addAuthorizationHeader($headers, $host, $requestUrl); - $this->addProxyAuthorizationHeader($headers, $requestUrl); - $this->calculateRequestLength($headers); - if ('1.1' == $this->request->getConfig('protocol_version')) { - $this->updateExpectHeader($headers); - } else { - $this->expect100Continue = false; - } - - $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' . - $this->request->getConfig('protocol_version') . "\r\n"; - foreach ($headers as $name => $value) { - $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); - $headersStr .= $canonicalName . ': ' . $value . "\r\n"; - } - return $headersStr . "\r\n"; - } - - /** - * Adds or removes 'Expect: 100-continue' header from request headers - * - * Also sets the $expect100Continue property. Parsing of existing header - * is somewhat needed due to its complex structure and due to the - * requirement in section 8.2.3 of RFC 2616: - * > A client MUST NOT send an Expect request-header field (section - * > 14.20) with the "100-continue" expectation if it does not intend - * > to send a request body. - * - * @param array &$headers Array of headers prepared for the request - * - * @throws HTTP_Request2_LogicException - * @link http://pear.php.net/bugs/bug.php?id=19233 - * @link http://tools.ietf.org/html/rfc2616#section-8.2.3 - */ - protected function updateExpectHeader(&$headers) - { - $this->expect100Continue = false; - $expectations = array(); - if (isset($headers['expect'])) { - if ('' === $headers['expect']) { - // empty 'Expect' header is technically invalid, so just get rid of it - unset($headers['expect']); - return; - } - // build regexp to parse the value of existing Expect header - $expectParam = ';\s*' . self::REGEXP_TOKEN . '(?:\s*=\s*(?:' - . self::REGEXP_TOKEN . '|' - . self::REGEXP_QUOTED_STRING . '))?\s*'; - $expectExtension = self::REGEXP_TOKEN . '(?:\s*=\s*(?:' - . self::REGEXP_TOKEN . '|' - . self::REGEXP_QUOTED_STRING . ')\s*(?:' - . $expectParam . ')*)?'; - $expectItem = '!(100-continue|' . $expectExtension . ')!A'; - - $pos = 0; - $length = strlen($headers['expect']); - - while ($pos < $length) { - $pos += strspn($headers['expect'], " \t", $pos); - if (',' === substr($headers['expect'], $pos, 1)) { - $pos++; - continue; - - } elseif (!preg_match($expectItem, $headers['expect'], $m, 0, $pos)) { - throw new HTTP_Request2_LogicException( - "Cannot parse value '{$headers['expect']}' of Expect header", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - - } else { - $pos += strlen($m[0]); - if (strcasecmp('100-continue', $m[0])) { - $expectations[] = $m[0]; - } - } - } - } - - if (1024 < $this->contentLength) { - $expectations[] = '100-continue'; - $this->expect100Continue = true; - } - - if (empty($expectations)) { - unset($headers['expect']); - } else { - $headers['expect'] = implode(',', $expectations); - } - } - - /** - * Sends the request body - * - * @throws HTTP_Request2_MessageException - */ - protected function writeBody() - { - if (in_array($this->request->getMethod(), self::$bodyDisallowed) - || 0 == $this->contentLength - ) { - return; - } - - $position = 0; - $bufferSize = $this->request->getConfig('buffer_size'); - $headers = $this->request->getHeaders(); - $chunked = isset($headers['transfer-encoding']); - while ($position < $this->contentLength) { - if (is_string($this->requestBody)) { - $str = substr($this->requestBody, $position, $bufferSize); - } elseif (is_resource($this->requestBody)) { - $str = fread($this->requestBody, $bufferSize); - } else { - $str = $this->requestBody->read($bufferSize); - } - if (!$chunked) { - $this->socket->write($str); - } else { - $this->socket->write(dechex(strlen($str)) . "\r\n{$str}\r\n"); - } - // Provide the length of written string to the observer, request #7630 - $this->request->setLastEvent('sentBodyPart', strlen($str)); - $position += strlen($str); - } - - // write zero-length chunk - if ($chunked) { - $this->socket->write("0\r\n\r\n"); - } - $this->request->setLastEvent('sentBody', $this->contentLength); - } - - /** - * Reads the remote server's response - * - * @return HTTP_Request2_Response - * @throws HTTP_Request2_Exception - */ - protected function readResponse() - { - $bufferSize = $this->request->getConfig('buffer_size'); - // http://tools.ietf.org/html/rfc2616#section-8.2.3 - // ...the client SHOULD NOT wait for an indefinite period before sending the request body - $timeout = $this->expect100Continue ? 1 : null; - - do { - try { - $response = new HTTP_Request2_Response( - $this->socket->readLine($bufferSize, $timeout), true, $this->request->getUrl() - ); - do { - $headerLine = $this->socket->readLine($bufferSize); - $response->parseHeaderLine($headerLine); - } while ('' != $headerLine); - - } catch (HTTP_Request2_MessageException $e) { - if (HTTP_Request2_Exception::TIMEOUT === $e->getCode() - && $this->expect100Continue - ) { - return null; - } - throw $e; - } - if ($this->expect100Continue && 100 == $response->getStatus()) { - return $response; - } - } while (in_array($response->getStatus(), array(100, 101))); - - $this->request->setLastEvent('receivedHeaders', $response); - - // No body possible in such responses - if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() - || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() - && 200 <= $response->getStatus() && 300 > $response->getStatus()) - || in_array($response->getStatus(), array(204, 304)) - ) { - return $response; - } - - $chunked = 'chunked' == $response->getHeader('transfer-encoding'); - $length = $response->getHeader('content-length'); - $hasBody = false; - if ($chunked || null === $length || 0 < intval($length)) { - // RFC 2616, section 4.4: - // 3. ... If a message is received with both a - // Transfer-Encoding header field and a Content-Length header field, - // the latter MUST be ignored. - $toRead = ($chunked || null === $length)? null: $length; - $this->chunkLength = 0; - - while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) { - if ($chunked) { - $data = $this->readChunked($bufferSize); - } elseif (is_null($toRead)) { - $data = $this->socket->read($bufferSize); - } else { - $data = $this->socket->read(min($toRead, $bufferSize)); - $toRead -= strlen($data); - } - if ('' == $data && (!$this->chunkLength || $this->socket->eof())) { - break; - } - - $hasBody = true; - if ($this->request->getConfig('store_body')) { - $response->appendBody($data); - } - if (!in_array($response->getHeader('content-encoding'), array('identity', null))) { - $this->request->setLastEvent('receivedEncodedBodyPart', $data); - } else { - $this->request->setLastEvent('receivedBodyPart', $data); - } - } - } - - if ($hasBody) { - $this->request->setLastEvent('receivedBody', $response); - } - return $response; - } - - /** - * Reads a part of response body encoded with chunked Transfer-Encoding - * - * @param int $bufferSize buffer size to use for reading - * - * @return string - * @throws HTTP_Request2_MessageException - */ - protected function readChunked($bufferSize) - { - // at start of the next chunk? - if (0 == $this->chunkLength) { - $line = $this->socket->readLine($bufferSize); - if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { - throw new HTTP_Request2_MessageException( - "Cannot decode chunked response, invalid chunk length '{$line}'", - HTTP_Request2_Exception::DECODE_ERROR - ); - } else { - $this->chunkLength = hexdec($matches[1]); - // Chunk with zero length indicates the end - if (0 == $this->chunkLength) { - $this->socket->readLine($bufferSize); - return ''; - } - } - } - $data = $this->socket->read(min($this->chunkLength, $bufferSize)); - $this->chunkLength -= strlen($data); - if (0 == $this->chunkLength) { - $this->socket->readLine($bufferSize); // Trailing CRLF - } - return $data; - } -} - + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Base class for HTTP_Request2 adapters */ +require_once 'HTTP/Request2/Adapter.php'; + +/** Socket wrapper class */ +require_once 'HTTP/Request2/SocketWrapper.php'; + +/** + * Socket-based adapter for HTTP_Request2 + * + * This adapter uses only PHP sockets and will work on almost any PHP + * environment. Code is based on original HTTP_Request PEAR package. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter +{ + /** + * Regular expression for 'token' rule from RFC 2616 + */ + const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+'; + + /** + * Regular expression for 'quoted-string' rule from RFC 2616 + */ + const REGEXP_QUOTED_STRING = '"(?>[^"\\\\]+|\\\\.)*"'; + + /** + * Connected sockets, needed for Keep-Alive support + * @var array + * @see connect() + */ + protected static $sockets = array(); + + /** + * Data for digest authentication scheme + * + * The keys for the array are URL prefixes. + * + * The values are associative arrays with data (realm, nonce, nonce-count, + * opaque...) needed for digest authentication. Stored here to prevent making + * duplicate requests to digest-protected resources after we have already + * received the challenge. + * + * @var array + */ + protected static $challenges = array(); + + /** + * Connected socket + * @var HTTP_Request2_SocketWrapper + * @see connect() + */ + protected $socket; + + /** + * Challenge used for server digest authentication + * @var array + */ + protected $serverChallenge; + + /** + * Challenge used for proxy digest authentication + * @var array + */ + protected $proxyChallenge; + + /** + * Remaining length of the current chunk, when reading chunked response + * @var integer + * @see readChunked() + */ + protected $chunkLength = 0; + + /** + * Remaining amount of redirections to follow + * + * Starts at 'max_redirects' configuration parameter and is reduced on each + * subsequent redirect. An Exception will be thrown once it reaches zero. + * + * @var integer + */ + protected $redirectCountdown = null; + + /** + * Whether to wait for "100 Continue" response before sending request body + * @var bool + */ + protected $expect100Continue = false; + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + $this->request = $request; + + try { + $keepAlive = $this->connect(); + $headers = $this->prepareHeaders(); + $this->socket->write($headers); + // provide request headers to the observer, see request #7633 + $this->request->setLastEvent('sentHeaders', $headers); + + if (!$this->expect100Continue) { + $this->writeBody(); + $response = $this->readResponse(); + + } else { + $response = $this->readResponse(); + if (!$response || 100 == $response->getStatus()) { + $this->expect100Continue = false; + // either got "100 Continue" or timed out -> send body + $this->writeBody(); + $response = $this->readResponse(); + } + } + + + if ($jar = $request->getCookieJar()) { + $jar->addCookiesFromResponse($response); + } + + if (!$this->canKeepAlive($keepAlive, $response)) { + $this->disconnect(); + } + + if ($this->shouldUseProxyDigestAuth($response)) { + return $this->sendRequest($request); + } + if ($this->shouldUseServerDigestAuth($response)) { + return $this->sendRequest($request); + } + if ($authInfo = $response->getHeader('authentication-info')) { + $this->updateChallenge($this->serverChallenge, $authInfo); + } + if ($proxyInfo = $response->getHeader('proxy-authentication-info')) { + $this->updateChallenge($this->proxyChallenge, $proxyInfo); + } + + } catch (Exception $e) { + $this->disconnect(); + } + + unset($this->request, $this->requestBody); + + if (!empty($e)) { + $this->redirectCountdown = null; + throw $e; + } + + if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) { + $this->redirectCountdown = null; + return $response; + } else { + return $this->handleRedirect($request, $response); + } + } + + /** + * Connects to the remote server + * + * @return bool whether the connection can be persistent + * @throws HTTP_Request2_Exception + */ + protected function connect() + { + $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https'); + $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + $headers = $this->request->getHeaders(); + $reqHost = $this->request->getUrl()->getHost(); + if (!($reqPort = $this->request->getUrl()->getPort())) { + $reqPort = $secure? 443: 80; + } + + $httpProxy = $socksProxy = false; + if (!($host = $this->request->getConfig('proxy_host'))) { + $host = $reqHost; + $port = $reqPort; + } else { + if (!($port = $this->request->getConfig('proxy_port'))) { + throw new HTTP_Request2_LogicException( + 'Proxy port not provided', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if ('http' == ($type = $this->request->getConfig('proxy_type'))) { + $httpProxy = true; + } elseif ('socks5' == $type) { + $socksProxy = true; + } else { + throw new HTTP_Request2_NotImplementedException( + "Proxy type '{$type}' is not supported" + ); + } + } + + if ($tunnel && !$httpProxy) { + throw new HTTP_Request2_LogicException( + "Trying to perform CONNECT request without proxy", + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if ($secure && !in_array('ssl', stream_get_transports())) { + throw new HTTP_Request2_LogicException( + 'Need OpenSSL support for https:// requests', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + + // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive + // connection token to a proxy server... + if ($httpProxy && !$secure && !empty($headers['connection']) + && 'Keep-Alive' == $headers['connection'] + ) { + $this->request->setHeader('connection'); + } + + $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && + empty($headers['connection'])) || + (!empty($headers['connection']) && + 'Keep-Alive' == $headers['connection']); + + $options = array(); + if ($ip = $this->request->getConfig('local_ip')) { + $options['socket'] = array( + 'bindto' => (false === strpos($ip, ':') ? $ip : '[' . $ip . ']') . ':0' + ); + } + if ($secure || $tunnel) { + $options['ssl'] = array(); + foreach ($this->request->getConfig() as $name => $value) { + if ('ssl_' == substr($name, 0, 4) && null !== $value) { + if ('ssl_verify_host' == $name) { + if (version_compare(phpversion(), '5.6', '<')) { + if ($value) { + $options['ssl']['CN_match'] = $reqHost; + } + + } else { + $options['ssl']['verify_peer_name'] = $value; + $options['ssl']['peer_name'] = $reqHost; + } + + } else { + $options['ssl'][substr($name, 4)] = $value; + } + } + } + ksort($options['ssl']); + } + + // Use global request timeout if given, see feature requests #5735, #8964 + if ($timeout = $this->request->getConfig('timeout')) { + $deadline = time() + $timeout; + } else { + $deadline = null; + } + + // Changing SSL context options after connection is established does *not* + // work, we need a new connection if options change + $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'tls://') + . $host . ':' . $port; + $socketKey = $remote . ( + ($secure && $httpProxy || $socksProxy) + ? "->{$reqHost}:{$reqPort}" : '' + ) . (empty($options)? '': ':' . serialize($options)); + unset($this->socket); + + // We use persistent connections and have a connected socket? + // Ensure that the socket is still connected, see bug #16149 + if ($keepAlive && !empty(self::$sockets[$socketKey]) + && !self::$sockets[$socketKey]->eof() + ) { + $this->socket =& self::$sockets[$socketKey]; + + } else { + if ($socksProxy) { + require_once 'HTTP/Request2/SOCKS5.php'; + + $this->socket = new HTTP_Request2_SOCKS5( + $remote, $this->request->getConfig('connect_timeout'), + $options, $this->request->getConfig('proxy_user'), + $this->request->getConfig('proxy_password') + ); + // handle request timeouts ASAP + $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); + $this->socket->connect($reqHost, $reqPort); + if (!$secure) { + $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}"; + } else { + $this->socket->enableCrypto(); + $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}"; + } + + } elseif ($secure && $httpProxy && !$tunnel) { + $this->establishTunnel(); + $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}"; + + } else { + $this->socket = new HTTP_Request2_SocketWrapper( + $remote, $this->request->getConfig('connect_timeout'), $options + ); + } + $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo); + self::$sockets[$socketKey] =& $this->socket; + } + $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); + return $keepAlive; + } + + /** + * Establishes a tunnel to a secure remote server via HTTP CONNECT request + * + * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP + * sees that we are connected to a proxy server (duh!) rather than the server + * that presents its certificate. + * + * @link http://tools.ietf.org/html/rfc2817#section-5.2 + * @throws HTTP_Request2_Exception + */ + protected function establishTunnel() + { + $donor = new self; + $connect = new HTTP_Request2( + $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT, + array_merge($this->request->getConfig(), array('adapter' => $donor)) + ); + $response = $connect->send(); + // Need any successful (2XX) response + if (200 > $response->getStatus() || 300 <= $response->getStatus()) { + throw new HTTP_Request2_ConnectionException( + 'Failed to connect via HTTPS proxy. Proxy response: ' . + $response->getStatus() . ' ' . $response->getReasonPhrase() + ); + } + $this->socket = $donor->socket; + $this->socket->enableCrypto(); + } + + /** + * Checks whether current connection may be reused or should be closed + * + * @param boolean $requestKeepAlive whether connection could + * be persistent in the first place + * @param HTTP_Request2_Response $response response object to check + * + * @return boolean + */ + protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response) + { + // Do not close socket on successful CONNECT request + if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() + && 200 <= $response->getStatus() && 300 > $response->getStatus() + ) { + return true; + } + + $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) + || null !== $response->getHeader('content-length') + // no body possible for such responses, see also request #17031 + || HTTP_Request2::METHOD_HEAD == $this->request->getMethod() + || in_array($response->getStatus(), array(204, 304)); + $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) || + (null === $response->getHeader('connection') && + '1.1' == $response->getVersion()); + return $requestKeepAlive && $lengthKnown && $persistent; + } + + /** + * Disconnects from the remote server + */ + protected function disconnect() + { + if (!empty($this->socket)) { + $this->socket = null; + $this->request->setLastEvent('disconnect'); + } + } + + /** + * Handles HTTP redirection + * + * This method will throw an Exception if redirect to a non-HTTP(S) location + * is attempted, also if number of redirects performed already is equal to + * 'max_redirects' configuration parameter. + * + * @param HTTP_Request2 $request Original request + * @param HTTP_Request2_Response $response Response containing redirect + * + * @return HTTP_Request2_Response Response from a new location + * @throws HTTP_Request2_Exception + */ + protected function handleRedirect( + HTTP_Request2 $request, HTTP_Request2_Response $response + ) { + if (is_null($this->redirectCountdown)) { + $this->redirectCountdown = $request->getConfig('max_redirects'); + } + if (0 == $this->redirectCountdown) { + $this->redirectCountdown = null; + // Copying cURL behaviour + throw new HTTP_Request2_MessageException( + 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed', + HTTP_Request2_Exception::TOO_MANY_REDIRECTS + ); + } + $redirectUrl = new Net_URL2( + $response->getHeader('location'), + array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets')) + ); + // refuse non-HTTP redirect + if ($redirectUrl->isAbsolute() + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + ) { + $this->redirectCountdown = null; + throw new HTTP_Request2_MessageException( + 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(), + HTTP_Request2_Exception::NON_HTTP_REDIRECT + ); + } + // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30), + // but in practice it is often not + if (!$redirectUrl->isAbsolute()) { + $redirectUrl = $request->getUrl()->resolve($redirectUrl); + } + $redirect = clone $request; + $redirect->setUrl($redirectUrl); + if (303 == $response->getStatus() + || (!$request->getConfig('strict_redirects') + && in_array($response->getStatus(), array(301, 302))) + ) { + $redirect->setMethod(HTTP_Request2::METHOD_GET); + $redirect->setBody(''); + } + + if (0 < $this->redirectCountdown) { + $this->redirectCountdown--; + } + return $this->sendRequest($redirect); + } + + /** + * Checks whether another request should be performed with server digest auth + * + * Several conditions should be satisfied for it to return true: + * - response status should be 401 + * - auth credentials should be set in the request object + * - response should contain WWW-Authenticate header with digest challenge + * - there is either no challenge stored for this URL or new challenge + * contains stale=true parameter (in other case we probably just failed + * due to invalid username / password) + * + * The method stores challenge values in $challenges static property + * + * @param HTTP_Request2_Response $response response to check + * + * @return boolean whether another request should be performed + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + */ + protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response) + { + // no sense repeating a request if we don't have credentials + if (401 != $response->getStatus() || !$this->request->getAuth()) { + return false; + } + if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) { + return false; + } + + $url = $this->request->getUrl(); + $scheme = $url->getScheme(); + $host = $scheme . '://' . $url->getHost(); + if ($port = $url->getPort()) { + if ((0 == strcasecmp($scheme, 'http') && 80 != $port) + || (0 == strcasecmp($scheme, 'https') && 443 != $port) + ) { + $host .= ':' . $port; + } + } + + if (!empty($challenge['domain'])) { + $prefixes = array(); + foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) { + // don't bother with different servers + if ('/' == substr($prefix, 0, 1)) { + $prefixes[] = $host . $prefix; + } + } + } + if (empty($prefixes)) { + $prefixes = array($host . '/'); + } + + $ret = true; + foreach ($prefixes as $prefix) { + if (!empty(self::$challenges[$prefix]) + && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + ) { + // probably credentials are invalid + $ret = false; + } + self::$challenges[$prefix] =& $challenge; + } + return $ret; + } + + /** + * Checks whether another request should be performed with proxy digest auth + * + * Several conditions should be satisfied for it to return true: + * - response status should be 407 + * - proxy auth credentials should be set in the request object + * - response should contain Proxy-Authenticate header with digest challenge + * - there is either no challenge stored for this proxy or new challenge + * contains stale=true parameter (in other case we probably just failed + * due to invalid username / password) + * + * The method stores challenge values in $challenges static property + * + * @param HTTP_Request2_Response $response response to check + * + * @return boolean whether another request should be performed + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + */ + protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response) + { + if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) { + return false; + } + if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) { + return false; + } + + $key = 'proxy://' . $this->request->getConfig('proxy_host') . + ':' . $this->request->getConfig('proxy_port'); + + if (!empty(self::$challenges[$key]) + && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + ) { + $ret = false; + } else { + $ret = true; + } + self::$challenges[$key] = $challenge; + return $ret; + } + + /** + * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value + * + * There is a problem with implementation of RFC 2617: several of the parameters + * are defined as quoted-string there and thus may contain backslash escaped + * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as + * just value of quoted-string X without surrounding quotes, it doesn't speak + * about removing backslash escaping. + * + * Now realm parameter is user-defined and human-readable, strange things + * happen when it contains quotes: + * - Apache allows quotes in realm, but apparently uses realm value without + * backslashes for digest computation + * - Squid allows (manually escaped) quotes there, but it is impossible to + * authorize with either escaped or unescaped quotes used in digest, + * probably it can't parse the response (?) + * - Both IE and Firefox display realm value with backslashes in + * the password popup and apparently use the same value for digest + * + * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in + * quoted-string handling, unfortunately that means failure to authorize + * sometimes + * + * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header + * + * @return mixed associative array with challenge parameters, false if + * no challenge is present in header value + * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters + */ + protected function parseDigestChallenge($headerValue) + { + $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')'; + $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!"; + if (!preg_match($challenge, $headerValue, $matches)) { + return false; + } + + preg_match_all('!' . $authParam . '!', $matches[0], $params); + $paramsAry = array(); + $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale', + 'algorithm', 'qop'); + for ($i = 0; $i < count($params[0]); $i++) { + // section 3.2.1: Any unrecognized directive MUST be ignored. + if (in_array($params[1][$i], $knownParams)) { + if ('"' == substr($params[2][$i], 0, 1)) { + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + } else { + $paramsAry[$params[1][$i]] = $params[2][$i]; + } + } + } + // we only support qop=auth + if (!empty($paramsAry['qop']) + && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))) + ) { + throw new HTTP_Request2_NotImplementedException( + "Only 'auth' qop is currently supported in digest authentication, " . + "server requested '{$paramsAry['qop']}'" + ); + } + // we only support algorithm=MD5 + if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) { + throw new HTTP_Request2_NotImplementedException( + "Only 'MD5' algorithm is currently supported in digest authentication, " . + "server requested '{$paramsAry['algorithm']}'" + ); + } + + return $paramsAry; + } + + /** + * Parses [Proxy-]Authentication-Info header value and updates challenge + * + * @param array &$challenge challenge to update + * @param string $headerValue value of [Proxy-]Authentication-Info header + * + * @todo validate server rspauth response + */ + protected function updateChallenge(&$challenge, $headerValue) + { + $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!'; + $paramsAry = array(); + + preg_match_all($authParam, $headerValue, $params); + for ($i = 0; $i < count($params[0]); $i++) { + if ('"' == substr($params[2][$i], 0, 1)) { + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + } else { + $paramsAry[$params[1][$i]] = $params[2][$i]; + } + } + // for now, just update the nonce value + if (!empty($paramsAry['nextnonce'])) { + $challenge['nonce'] = $paramsAry['nextnonce']; + $challenge['nc'] = 1; + } + } + + /** + * Creates a value for [Proxy-]Authorization header when using digest authentication + * + * @param string $user user name + * @param string $password password + * @param string $url request URL + * @param array &$challenge digest challenge parameters + * + * @return string value of [Proxy-]Authorization request header + * @link http://tools.ietf.org/html/rfc2617#section-3.2.2 + */ + protected function createDigestResponse($user, $password, $url, &$challenge) + { + if (false !== ($q = strpos($url, '?')) + && $this->request->getConfig('digest_compat_ie') + ) { + $url = substr($url, 0, $q); + } + + $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password); + $a2 = md5($this->request->getMethod() . ':' . $url); + + if (empty($challenge['qop'])) { + $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2); + } else { + $challenge['cnonce'] = 'Req2.' . rand(); + if (empty($challenge['nc'])) { + $challenge['nc'] = 1; + } + $nc = sprintf('%08x', $challenge['nc']++); + $digest = md5( + $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . + $challenge['cnonce'] . ':auth:' . $a2 + ); + } + return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' . + 'realm="' . $challenge['realm'] . '", ' . + 'nonce="' . $challenge['nonce'] . '", ' . + 'uri="' . $url . '", ' . + 'response="' . $digest . '"' . + (!empty($challenge['opaque'])? + ', opaque="' . $challenge['opaque'] . '"': + '') . + (!empty($challenge['qop'])? + ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"': + ''); + } + + /** + * Adds 'Authorization' header (if needed) to request headers array + * + * @param array &$headers request headers + * @param string $requestHost request host (needed for digest authentication) + * @param string $requestUrl request URL (needed for digest authentication) + * + * @throws HTTP_Request2_NotImplementedException + */ + protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl) + { + if (!($auth = $this->request->getAuth())) { + return; + } + switch ($auth['scheme']) { + case HTTP_Request2::AUTH_BASIC: + $headers['authorization'] = 'Basic ' . base64_encode( + $auth['user'] . ':' . $auth['password'] + ); + break; + + case HTTP_Request2::AUTH_DIGEST: + unset($this->serverChallenge); + $fullUrl = ('/' == $requestUrl[0])? + $this->request->getUrl()->getScheme() . '://' . + $requestHost . $requestUrl: + $requestUrl; + foreach (array_keys(self::$challenges) as $key) { + if ($key == substr($fullUrl, 0, strlen($key))) { + $headers['authorization'] = $this->createDigestResponse( + $auth['user'], $auth['password'], + $requestUrl, self::$challenges[$key] + ); + $this->serverChallenge =& self::$challenges[$key]; + break; + } + } + break; + + default: + throw new HTTP_Request2_NotImplementedException( + "Unknown HTTP authentication scheme '{$auth['scheme']}'" + ); + } + } + + /** + * Adds 'Proxy-Authorization' header (if needed) to request headers array + * + * @param array &$headers request headers + * @param string $requestUrl request URL (needed for digest authentication) + * + * @throws HTTP_Request2_NotImplementedException + */ + protected function addProxyAuthorizationHeader(&$headers, $requestUrl) + { + if (!$this->request->getConfig('proxy_host') + || !($user = $this->request->getConfig('proxy_user')) + || (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) + && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod()) + ) { + return; + } + + $password = $this->request->getConfig('proxy_password'); + switch ($this->request->getConfig('proxy_auth_scheme')) { + case HTTP_Request2::AUTH_BASIC: + $headers['proxy-authorization'] = 'Basic ' . base64_encode( + $user . ':' . $password + ); + break; + + case HTTP_Request2::AUTH_DIGEST: + unset($this->proxyChallenge); + $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') . + ':' . $this->request->getConfig('proxy_port'); + if (!empty(self::$challenges[$proxyUrl])) { + $headers['proxy-authorization'] = $this->createDigestResponse( + $user, $password, + $requestUrl, self::$challenges[$proxyUrl] + ); + $this->proxyChallenge =& self::$challenges[$proxyUrl]; + } + break; + + default: + throw new HTTP_Request2_NotImplementedException( + "Unknown HTTP authentication scheme '" . + $this->request->getConfig('proxy_auth_scheme') . "'" + ); + } + } + + + /** + * Creates the string with the Request-Line and request headers + * + * @return string + * @throws HTTP_Request2_Exception + */ + protected function prepareHeaders() + { + $headers = $this->request->getHeaders(); + $url = $this->request->getUrl(); + $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + $host = $url->getHost(); + + $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80; + if (($port = $url->getPort()) && $port != $defaultPort || $connect) { + $host .= ':' . (empty($port)? $defaultPort: $port); + } + // Do not overwrite explicitly set 'Host' header, see bug #16146 + if (!isset($headers['host'])) { + $headers['host'] = $host; + } + + if ($connect) { + $requestUrl = $host; + + } else { + if (!$this->request->getConfig('proxy_host') + || 'http' != $this->request->getConfig('proxy_type') + || 0 == strcasecmp($url->getScheme(), 'https') + ) { + $requestUrl = ''; + } else { + $requestUrl = $url->getScheme() . '://' . $host; + } + $path = $url->getPath(); + $query = $url->getQuery(); + $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query); + } + + if ('1.1' == $this->request->getConfig('protocol_version') + && extension_loaded('zlib') && !isset($headers['accept-encoding']) + ) { + $headers['accept-encoding'] = 'gzip, deflate'; + } + if (($jar = $this->request->getCookieJar()) + && ($cookies = $jar->getMatching($this->request->getUrl(), true)) + ) { + $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; + } + + $this->addAuthorizationHeader($headers, $host, $requestUrl); + $this->addProxyAuthorizationHeader($headers, $requestUrl); + $this->calculateRequestLength($headers); + if ('1.1' == $this->request->getConfig('protocol_version')) { + $this->updateExpectHeader($headers); + } else { + $this->expect100Continue = false; + } + + $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' . + $this->request->getConfig('protocol_version') . "\r\n"; + foreach ($headers as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $headersStr .= $canonicalName . ': ' . $value . "\r\n"; + } + return $headersStr . "\r\n"; + } + + /** + * Adds or removes 'Expect: 100-continue' header from request headers + * + * Also sets the $expect100Continue property. Parsing of existing header + * is somewhat needed due to its complex structure and due to the + * requirement in section 8.2.3 of RFC 2616: + * > A client MUST NOT send an Expect request-header field (section + * > 14.20) with the "100-continue" expectation if it does not intend + * > to send a request body. + * + * @param array &$headers Array of headers prepared for the request + * + * @throws HTTP_Request2_LogicException + * @link http://pear.php.net/bugs/bug.php?id=19233 + * @link http://tools.ietf.org/html/rfc2616#section-8.2.3 + */ + protected function updateExpectHeader(&$headers) + { + $this->expect100Continue = false; + $expectations = array(); + if (isset($headers['expect'])) { + if ('' === $headers['expect']) { + // empty 'Expect' header is technically invalid, so just get rid of it + unset($headers['expect']); + return; + } + // build regexp to parse the value of existing Expect header + $expectParam = ';\s*' . self::REGEXP_TOKEN . '(?:\s*=\s*(?:' + . self::REGEXP_TOKEN . '|' + . self::REGEXP_QUOTED_STRING . '))?\s*'; + $expectExtension = self::REGEXP_TOKEN . '(?:\s*=\s*(?:' + . self::REGEXP_TOKEN . '|' + . self::REGEXP_QUOTED_STRING . ')\s*(?:' + . $expectParam . ')*)?'; + $expectItem = '!(100-continue|' . $expectExtension . ')!A'; + + $pos = 0; + $length = strlen($headers['expect']); + + while ($pos < $length) { + $pos += strspn($headers['expect'], " \t", $pos); + if (',' === substr($headers['expect'], $pos, 1)) { + $pos++; + continue; + + } elseif (!preg_match($expectItem, $headers['expect'], $m, 0, $pos)) { + throw new HTTP_Request2_LogicException( + "Cannot parse value '{$headers['expect']}' of Expect header", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + + } else { + $pos += strlen($m[0]); + if (strcasecmp('100-continue', $m[0])) { + $expectations[] = $m[0]; + } + } + } + } + + if (1024 < $this->contentLength) { + $expectations[] = '100-continue'; + $this->expect100Continue = true; + } + + if (empty($expectations)) { + unset($headers['expect']); + } else { + $headers['expect'] = implode(',', $expectations); + } + } + + /** + * Sends the request body + * + * @throws HTTP_Request2_MessageException + */ + protected function writeBody() + { + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + || 0 == $this->contentLength + ) { + return; + } + + $position = 0; + $bufferSize = $this->request->getConfig('buffer_size'); + $headers = $this->request->getHeaders(); + $chunked = isset($headers['transfer-encoding']); + while ($position < $this->contentLength) { + if (is_string($this->requestBody)) { + $str = substr($this->requestBody, $position, $bufferSize); + } elseif (is_resource($this->requestBody)) { + $str = fread($this->requestBody, $bufferSize); + } else { + $str = $this->requestBody->read($bufferSize); + } + if (!$chunked) { + $this->socket->write($str); + } else { + $this->socket->write(dechex(strlen($str)) . "\r\n{$str}\r\n"); + } + // Provide the length of written string to the observer, request #7630 + $this->request->setLastEvent('sentBodyPart', strlen($str)); + $position += strlen($str); + } + + // write zero-length chunk + if ($chunked) { + $this->socket->write("0\r\n\r\n"); + } + $this->request->setLastEvent('sentBody', $this->contentLength); + } + + /** + * Reads the remote server's response + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + protected function readResponse() + { + $bufferSize = $this->request->getConfig('buffer_size'); + // http://tools.ietf.org/html/rfc2616#section-8.2.3 + // ...the client SHOULD NOT wait for an indefinite period before sending the request body + $timeout = $this->expect100Continue ? 1 : null; + + do { + try { + $response = new HTTP_Request2_Response( + $this->socket->readLine($bufferSize, $timeout), true, $this->request->getUrl() + ); + do { + $headerLine = $this->socket->readLine($bufferSize); + $response->parseHeaderLine($headerLine); + } while ('' != $headerLine); + + } catch (HTTP_Request2_MessageException $e) { + if (HTTP_Request2_Exception::TIMEOUT === $e->getCode() + && $this->expect100Continue + ) { + return null; + } + throw $e; + } + if ($this->expect100Continue && 100 == $response->getStatus()) { + return $response; + } + } while (in_array($response->getStatus(), array(100, 101))); + + $this->request->setLastEvent('receivedHeaders', $response); + + // No body possible in such responses + if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() + || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() + && 200 <= $response->getStatus() && 300 > $response->getStatus()) + || in_array($response->getStatus(), array(204, 304)) + ) { + return $response; + } + + $chunked = 'chunked' == $response->getHeader('transfer-encoding'); + $length = $response->getHeader('content-length'); + $hasBody = false; + // RFC 2616, section 4.4: + // 3. ... If a message is received with both a + // Transfer-Encoding header field and a Content-Length header field, + // the latter MUST be ignored. + $toRead = ($chunked || null === $length)? null: $length; + $this->chunkLength = 0; + + if ($chunked || null === $length || 0 < intval($length)) { + while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) { + if ($chunked) { + $data = $this->readChunked($bufferSize); + } elseif (is_null($toRead)) { + $data = $this->socket->read($bufferSize); + } else { + $data = $this->socket->read(min($toRead, $bufferSize)); + $toRead -= strlen($data); + } + if ('' == $data && (!$this->chunkLength || $this->socket->eof())) { + break; + } + + $hasBody = true; + if ($this->request->getConfig('store_body')) { + $response->appendBody($data); + } + if (!in_array($response->getHeader('content-encoding'), array('identity', null))) { + $this->request->setLastEvent('receivedEncodedBodyPart', $data); + } else { + $this->request->setLastEvent('receivedBodyPart', $data); + } + } + } + if (0 !== $this->chunkLength || null !== $toRead && $toRead > 0) { + $this->request->setLastEvent( + 'warning', 'transfer closed with outstanding read data remaining' + ); + } + + if ($hasBody) { + $this->request->setLastEvent('receivedBody', $response); + } + return $response; + } + + /** + * Reads a part of response body encoded with chunked Transfer-Encoding + * + * @param int $bufferSize buffer size to use for reading + * + * @return string + * @throws HTTP_Request2_MessageException + */ + protected function readChunked($bufferSize) + { + // at start of the next chunk? + if (0 == $this->chunkLength) { + $line = $this->socket->readLine($bufferSize); + if ('' === $line && $this->socket->eof()) { + $this->chunkLength = -1; // indicate missing chunk + return ''; + + } elseif (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + throw new HTTP_Request2_MessageException( + "Cannot decode chunked response, invalid chunk length '{$line}'", + HTTP_Request2_Exception::DECODE_ERROR + ); + + } else { + $this->chunkLength = hexdec($matches[1]); + // Chunk with zero length indicates the end + if (0 == $this->chunkLength) { + $this->socket->readLine($bufferSize); + return ''; + } + } + } + $data = $this->socket->read(min($this->chunkLength, $bufferSize)); + $this->chunkLength -= strlen($data); + if (0 == $this->chunkLength) { + $this->socket->readLine($bufferSize); // Trailing CRLF + } + return $data; + } +} + ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/CookieJar.php b/bundled-libs/HTTP/Request2/CookieJar.php index ed6352eb..f191e255 100644 --- a/bundled-libs/HTTP/Request2/CookieJar.php +++ b/bundled-libs/HTTP/Request2/CookieJar.php @@ -1,494 +1,547 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** Class representing a HTTP request message */ -require_once 'HTTP/Request2.php'; - -/** - * Stores cookies and passes them between HTTP requests - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: @package_version@ - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_CookieJar implements Serializable -{ - /** - * Array of stored cookies - * - * The array is indexed by domain, path and cookie name - * .example.com - * / - * some_cookie => cookie data - * /subdir - * other_cookie => cookie data - * .example.org - * ... - * - * @var array - */ - protected $cookies = array(); - - /** - * Whether session cookies should be serialized when serializing the jar - * @var bool - */ - protected $serializeSession = false; - - /** - * Whether Public Suffix List should be used for domain matching - * @var bool - */ - protected $useList = true; - - /** - * Array with Public Suffix List data - * @var array - * @link http://publicsuffix.org/ - */ - protected static $psl = array(); - - /** - * Class constructor, sets various options - * - * @param bool $serializeSessionCookies Controls serializing session cookies, - * see {@link serializeSessionCookies()} - * @param bool $usePublicSuffixList Controls using Public Suffix List, - * see {@link usePublicSuffixList()} - */ - public function __construct( - $serializeSessionCookies = false, $usePublicSuffixList = true - ) { - $this->serializeSessionCookies($serializeSessionCookies); - $this->usePublicSuffixList($usePublicSuffixList); - } - - /** - * Returns current time formatted in ISO-8601 at UTC timezone - * - * @return string - */ - protected function now() - { - $dt = new DateTime(); - $dt->setTimezone(new DateTimeZone('UTC')); - return $dt->format(DateTime::ISO8601); - } - - /** - * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields - * - * The checks are as follows: - * - cookie array should contain 'name' and 'value' fields; - * - name and value should not contain disallowed symbols; - * - 'expires' should be either empty parseable by DateTime; - * - 'domain' and 'path' should be either not empty or an URL where - * cookie was set should be provided. - * - if $setter is provided, then document at that URL should be allowed - * to set a cookie for that 'domain'. If $setter is not provided, - * then no domain checks will be made. - * - * 'expires' field will be converted to ISO8601 format from COOKIE format, - * 'domain' and 'path' will be set from setter URL if empty. - * - * @param array $cookie cookie data, as returned by - * {@link HTTP_Request2_Response::getCookies()} - * @param Net_URL2 $setter URL of the document that sent Set-Cookie header - * - * @return array Updated cookie array - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_MessageException - */ - protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null) - { - if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) { - throw new HTTP_Request2_LogicException( - "Cookie array should contain 'name' and 'value' fields", - HTTP_Request2_Exception::MISSING_VALUE - ); - } - if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) { - throw new HTTP_Request2_LogicException( - "Invalid cookie name: '{$cookie['name']}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) { - throw new HTTP_Request2_LogicException( - "Invalid cookie value: '{$cookie['value']}'", - HTTP_Request2_Exception::INVALID_ARGUMENT - ); - } - $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false); - - // Need ISO-8601 date @ UTC timezone - if (!empty($cookie['expires']) - && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires']) - ) { - try { - $dt = new DateTime($cookie['expires']); - $dt->setTimezone(new DateTimeZone('UTC')); - $cookie['expires'] = $dt->format(DateTime::ISO8601); - } catch (Exception $e) { - throw new HTTP_Request2_LogicException($e->getMessage()); - } - } - - if (empty($cookie['domain']) || empty($cookie['path'])) { - if (!$setter) { - throw new HTTP_Request2_LogicException( - 'Cookie misses domain and/or path component, cookie setter URL needed', - HTTP_Request2_Exception::MISSING_VALUE - ); - } - if (empty($cookie['domain'])) { - if ($host = $setter->getHost()) { - $cookie['domain'] = $host; - } else { - throw new HTTP_Request2_LogicException( - 'Setter URL does not contain host part, can\'t set cookie domain', - HTTP_Request2_Exception::MISSING_VALUE - ); - } - } - if (empty($cookie['path'])) { - $path = $setter->getPath(); - $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1); - } - } - - if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) { - throw new HTTP_Request2_MessageException( - "Domain " . $setter->getHost() . " cannot set cookies for " - . $cookie['domain'] - ); - } - - return $cookie; - } - - /** - * Stores a cookie in the jar - * - * @param array $cookie cookie data, as returned by - * {@link HTTP_Request2_Response::getCookies()} - * @param Net_URL2 $setter URL of the document that sent Set-Cookie header - * - * @throws HTTP_Request2_Exception - */ - public function store(array $cookie, Net_URL2 $setter = null) - { - $cookie = $this->checkAndUpdateFields($cookie, $setter); - - if (strlen($cookie['value']) - && (is_null($cookie['expires']) || $cookie['expires'] > $this->now()) - ) { - if (!isset($this->cookies[$cookie['domain']])) { - $this->cookies[$cookie['domain']] = array(); - } - if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { - $this->cookies[$cookie['domain']][$cookie['path']] = array(); - } - $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; - - } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) { - unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]); - } - } - - /** - * Adds cookies set in HTTP response to the jar - * - * @param HTTP_Request2_Response $response HTTP response message - * @param Net_URL2 $setter original request URL, needed for - * setting default domain/path - */ - public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter) - { - foreach ($response->getCookies() as $cookie) { - $this->store($cookie, $setter); - } - } - - /** - * Returns all cookies matching a given request URL - * - * The following checks are made: - * - cookie domain should match request host - * - cookie path should be a prefix for request path - * - 'secure' cookies will only be sent for HTTPS requests - * - * @param Net_URL2 $url Request url - * @param bool $asString Whether to return cookies as string for "Cookie: " header - * - * @return array|string Matching cookies - */ - public function getMatching(Net_URL2 $url, $asString = false) - { - $host = $url->getHost(); - $path = $url->getPath(); - $secure = 0 == strcasecmp($url->getScheme(), 'https'); - - $matched = $ret = array(); - foreach (array_keys($this->cookies) as $domain) { - if ($this->domainMatch($host, $domain)) { - foreach (array_keys($this->cookies[$domain]) as $cPath) { - if (0 === strpos($path, $cPath)) { - foreach ($this->cookies[$domain][$cPath] as $name => $cookie) { - if (!$cookie['secure'] || $secure) { - $matched[$name][strlen($cookie['path'])] = $cookie; - } - } - } - } - } - } - foreach ($matched as $cookies) { - krsort($cookies); - $ret = array_merge($ret, $cookies); - } - if (!$asString) { - return $ret; - } else { - $str = ''; - foreach ($ret as $c) { - $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value']; - } - return $str; - } - } - - /** - * Returns all cookies stored in a jar - * - * @return array - */ - public function getAll() - { - $cookies = array(); - foreach (array_keys($this->cookies) as $domain) { - foreach (array_keys($this->cookies[$domain]) as $path) { - foreach ($this->cookies[$domain][$path] as $name => $cookie) { - $cookies[] = $cookie; - } - } - } - return $cookies; - } - - /** - * Sets whether session cookies should be serialized when serializing the jar - * - * @param boolean $serialize serialize? - */ - public function serializeSessionCookies($serialize) - { - $this->serializeSession = (bool)$serialize; - } - - /** - * Sets whether Public Suffix List should be used for restricting cookie-setting - * - * Without PSL {@link domainMatch()} will only prevent setting cookies for - * top-level domains like '.com' or '.org'. However, it will not prevent - * setting a cookie for '.co.uk' even though only third-level registrations - * are possible in .uk domain. - * - * With the List it is possible to find the highest level at which a domain - * may be registered for a particular top-level domain and consequently - * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by - * Firefox, Chrome and Opera browsers to restrict cookie setting. - * - * Note that PSL is licensed differently to HTTP_Request2 package (refer to - * the license information in public-suffix-list.php), so you can disable - * its use if this is an issue for you. - * - * @param boolean $useList use the list? - * - * @link http://publicsuffix.org/learn/ - */ - public function usePublicSuffixList($useList) - { - $this->useList = (bool)$useList; - } - - /** - * Returns string representation of object - * - * @return string - * - * @see Serializable::serialize() - */ - public function serialize() - { - $cookies = $this->getAll(); - if (!$this->serializeSession) { - for ($i = count($cookies) - 1; $i >= 0; $i--) { - if (empty($cookies[$i]['expires'])) { - unset($cookies[$i]); - } - } - } - return serialize(array( - 'cookies' => $cookies, - 'serializeSession' => $this->serializeSession, - 'useList' => $this->useList - )); - } - - /** - * Constructs the object from serialized string - * - * @param string $serialized string representation - * - * @see Serializable::unserialize() - */ - public function unserialize($serialized) - { - $data = unserialize($serialized); - $now = $this->now(); - $this->serializeSessionCookies($data['serializeSession']); - $this->usePublicSuffixList($data['useList']); - foreach ($data['cookies'] as $cookie) { - if (!empty($cookie['expires']) && $cookie['expires'] <= $now) { - continue; - } - if (!isset($this->cookies[$cookie['domain']])) { - $this->cookies[$cookie['domain']] = array(); - } - if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { - $this->cookies[$cookie['domain']][$cookie['path']] = array(); - } - $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; - } - } - - /** - * Checks whether a cookie domain matches a request host. - * - * The method is used by {@link store()} to check for whether a document - * at given URL can set a cookie with a given domain attribute and by - * {@link getMatching()} to find cookies matching the request URL. - * - * @param string $requestHost request host - * @param string $cookieDomain cookie domain - * - * @return bool match success - */ - public function domainMatch($requestHost, $cookieDomain) - { - if ($requestHost == $cookieDomain) { - return true; - } - // IP address, we require exact match - if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) { - return false; - } - if ('.' != $cookieDomain[0]) { - $cookieDomain = '.' . $cookieDomain; - } - // prevents setting cookies for '.com' and similar domains - if (!$this->useList && substr_count($cookieDomain, '.') < 2 - || $this->useList && !self::getRegisteredDomain($cookieDomain) - ) { - return false; - } - return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain; - } - - /** - * Removes subdomains to get the registered domain (the first after top-level) - * - * The method will check Public Suffix List to find out where top-level - * domain ends and registered domain starts. It will remove domain parts - * to the left of registered one. - * - * @param string $domain domain name - * - * @return string|bool registered domain, will return false if $domain is - * either invalid or a TLD itself - */ - public static function getRegisteredDomain($domain) - { - $domainParts = explode('.', ltrim($domain, '.')); - - // load the list if needed - if (empty(self::$psl)) { - $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2'; - if (0 === strpos($path, '@' . 'data_dir@')) { - $path = realpath( - dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' - . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' - ); - } - self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php'; - } - - if (!($result = self::checkDomainsList($domainParts, self::$psl))) { - // known TLD, invalid domain name - return false; - } - - // unknown TLD - if (!strpos($result, '.')) { - // fallback to checking that domain "has at least two dots" - if (2 > ($count = count($domainParts))) { - return false; - } - return $domainParts[$count - 2] . '.' . $domainParts[$count - 1]; - } - return $result; - } - - /** - * Recursive helper method for {@link getRegisteredDomain()} - * - * @param array $domainParts remaining domain parts - * @param mixed $listNode node in {@link HTTP_Request2_CookieJar::$psl} to check - * - * @return string|null concatenated domain parts, null in case of error - */ - protected static function checkDomainsList(array $domainParts, $listNode) - { - $sub = array_pop($domainParts); - $result = null; - - if (!is_array($listNode) || is_null($sub) - || array_key_exists('!' . $sub, $listNode) - ) { - return $sub; - - } elseif (array_key_exists($sub, $listNode)) { - $result = self::checkDomainsList($domainParts, $listNode[$sub]); - - } elseif (array_key_exists('*', $listNode)) { - $result = self::checkDomainsList($domainParts, $listNode['*']); - - } else { - return $sub; - } - - return (strlen($result) > 0) ? ($result . '.' . $sub) : null; - } -} + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Class representing a HTTP request message */ +require_once 'HTTP/Request2.php'; + +/** + * Stores cookies and passes them between HTTP requests + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: @package_version@ + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_CookieJar implements Serializable +{ + /** + * Array of stored cookies + * + * The array is indexed by domain, path and cookie name + * .example.com + * / + * some_cookie => cookie data + * /subdir + * other_cookie => cookie data + * .example.org + * ... + * + * @var array + */ + protected $cookies = array(); + + /** + * Whether session cookies should be serialized when serializing the jar + * @var bool + */ + protected $serializeSession = false; + + /** + * Whether Public Suffix List should be used for domain matching + * @var bool + */ + protected $useList = true; + + /** + * Whether an attempt to store an invalid cookie should be ignored, rather than cause an Exception + * @var bool + */ + protected $ignoreInvalid = false; + + /** + * Array with Public Suffix List data + * @var array + * @link http://publicsuffix.org/ + */ + protected static $psl = array(); + + /** + * Class constructor, sets various options + * + * @param bool $serializeSessionCookies Controls serializing session cookies, + * see {@link serializeSessionCookies()} + * @param bool $usePublicSuffixList Controls using Public Suffix List, + * see {@link usePublicSuffixList()} + * @param bool $ignoreInvalidCookies Whether invalid cookies should be ignored, + * see {@link ignoreInvalidCookies()} + */ + public function __construct( + $serializeSessionCookies = false, $usePublicSuffixList = true, + $ignoreInvalidCookies = false + ) { + $this->serializeSessionCookies($serializeSessionCookies); + $this->usePublicSuffixList($usePublicSuffixList); + $this->ignoreInvalidCookies($ignoreInvalidCookies); + } + + /** + * Returns current time formatted in ISO-8601 at UTC timezone + * + * @return string + */ + protected function now() + { + $dt = new DateTime(); + $dt->setTimezone(new DateTimeZone('UTC')); + return $dt->format(DateTime::ISO8601); + } + + /** + * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields + * + * The checks are as follows: + * - cookie array should contain 'name' and 'value' fields; + * - name and value should not contain disallowed symbols; + * - 'expires' should be either empty parseable by DateTime; + * - 'domain' and 'path' should be either not empty or an URL where + * cookie was set should be provided. + * - if $setter is provided, then document at that URL should be allowed + * to set a cookie for that 'domain'. If $setter is not provided, + * then no domain checks will be made. + * + * 'expires' field will be converted to ISO8601 format from COOKIE format, + * 'domain' and 'path' will be set from setter URL if empty. + * + * @param array $cookie cookie data, as returned by + * {@link HTTP_Request2_Response::getCookies()} + * @param Net_URL2 $setter URL of the document that sent Set-Cookie header + * + * @return array Updated cookie array + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_MessageException + */ + protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null) + { + if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) { + throw new HTTP_Request2_LogicException( + "Cookie array should contain 'name' and 'value' fields", + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) { + throw new HTTP_Request2_LogicException( + "Invalid cookie name: '{$cookie['name']}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) { + throw new HTTP_Request2_LogicException( + "Invalid cookie value: '{$cookie['value']}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false); + + // Need ISO-8601 date @ UTC timezone + if (!empty($cookie['expires']) + && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires']) + ) { + try { + $dt = new DateTime($cookie['expires']); + $dt->setTimezone(new DateTimeZone('UTC')); + $cookie['expires'] = $dt->format(DateTime::ISO8601); + } catch (Exception $e) { + throw new HTTP_Request2_LogicException($e->getMessage()); + } + } + + if (empty($cookie['domain']) || empty($cookie['path'])) { + if (!$setter) { + throw new HTTP_Request2_LogicException( + 'Cookie misses domain and/or path component, cookie setter URL needed', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if (empty($cookie['domain'])) { + if ($host = $setter->getHost()) { + $cookie['domain'] = $host; + } else { + throw new HTTP_Request2_LogicException( + 'Setter URL does not contain host part, can\'t set cookie domain', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + } + if (empty($cookie['path'])) { + $path = $setter->getPath(); + $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1); + } + } + + if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) { + throw new HTTP_Request2_MessageException( + "Domain " . $setter->getHost() . " cannot set cookies for " + . $cookie['domain'] + ); + } + + return $cookie; + } + + /** + * Stores a cookie in the jar + * + * @param array $cookie cookie data, as returned by + * {@link HTTP_Request2_Response::getCookies()} + * @param Net_URL2 $setter URL of the document that sent Set-Cookie header + * + * @return bool whether the cookie was successfully stored + * @throws HTTP_Request2_Exception + */ + public function store(array $cookie, Net_URL2 $setter = null) + { + try { + $cookie = $this->checkAndUpdateFields($cookie, $setter); + } catch (HTTP_Request2_Exception $e) { + if ($this->ignoreInvalid) { + return false; + } else { + throw $e; + } + } + + if (strlen($cookie['value']) + && (is_null($cookie['expires']) || $cookie['expires'] > $this->now()) + ) { + if (!isset($this->cookies[$cookie['domain']])) { + $this->cookies[$cookie['domain']] = array(); + } + if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { + $this->cookies[$cookie['domain']][$cookie['path']] = array(); + } + $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; + + } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) { + unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]); + } + + return true; + } + + /** + * Adds cookies set in HTTP response to the jar + * + * @param HTTP_Request2_Response $response HTTP response message + * @param Net_URL2 $setter original request URL, needed for + * setting default domain/path. If not given, + * effective URL from response will be used. + * + * @return bool whether all cookies were successfully stored + * @throws HTTP_Request2_LogicException + */ + public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter = null) + { + if (null === $setter) { + if (!($effectiveUrl = $response->getEffectiveUrl())) { + throw new HTTP_Request2_LogicException( + 'Response URL required for adding cookies from response', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + $setter = new Net_URL2($effectiveUrl); + } + + $success = true; + foreach ($response->getCookies() as $cookie) { + $success = $this->store($cookie, $setter) && $success; + } + return $success; + } + + /** + * Returns all cookies matching a given request URL + * + * The following checks are made: + * - cookie domain should match request host + * - cookie path should be a prefix for request path + * - 'secure' cookies will only be sent for HTTPS requests + * + * @param Net_URL2 $url Request url + * @param bool $asString Whether to return cookies as string for "Cookie: " header + * + * @return array|string Matching cookies + */ + public function getMatching(Net_URL2 $url, $asString = false) + { + $host = $url->getHost(); + $path = $url->getPath(); + $secure = 0 == strcasecmp($url->getScheme(), 'https'); + + $matched = $ret = array(); + foreach (array_keys($this->cookies) as $domain) { + if ($this->domainMatch($host, $domain)) { + foreach (array_keys($this->cookies[$domain]) as $cPath) { + if (0 === strpos($path, $cPath)) { + foreach ($this->cookies[$domain][$cPath] as $name => $cookie) { + if (!$cookie['secure'] || $secure) { + $matched[$name][strlen($cookie['path'])] = $cookie; + } + } + } + } + } + } + foreach ($matched as $cookies) { + krsort($cookies); + $ret = array_merge($ret, $cookies); + } + if (!$asString) { + return $ret; + } else { + $str = ''; + foreach ($ret as $c) { + $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value']; + } + return $str; + } + } + + /** + * Returns all cookies stored in a jar + * + * @return array + */ + public function getAll() + { + $cookies = array(); + foreach (array_keys($this->cookies) as $domain) { + foreach (array_keys($this->cookies[$domain]) as $path) { + foreach ($this->cookies[$domain][$path] as $name => $cookie) { + $cookies[] = $cookie; + } + } + } + return $cookies; + } + + /** + * Sets whether session cookies should be serialized when serializing the jar + * + * @param boolean $serialize serialize? + */ + public function serializeSessionCookies($serialize) + { + $this->serializeSession = (bool)$serialize; + } + + /** + * Sets whether invalid cookies should be silently ignored or cause an Exception + * + * @param boolean $ignore ignore? + * @link http://pear.php.net/bugs/bug.php?id=19937 + * @link http://pear.php.net/bugs/bug.php?id=20401 + */ + public function ignoreInvalidCookies($ignore) + { + $this->ignoreInvalid = (bool)$ignore; + } + + /** + * Sets whether Public Suffix List should be used for restricting cookie-setting + * + * Without PSL {@link domainMatch()} will only prevent setting cookies for + * top-level domains like '.com' or '.org'. However, it will not prevent + * setting a cookie for '.co.uk' even though only third-level registrations + * are possible in .uk domain. + * + * With the List it is possible to find the highest level at which a domain + * may be registered for a particular top-level domain and consequently + * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by + * Firefox, Chrome and Opera browsers to restrict cookie setting. + * + * Note that PSL is licensed differently to HTTP_Request2 package (refer to + * the license information in public-suffix-list.php), so you can disable + * its use if this is an issue for you. + * + * @param boolean $useList use the list? + * + * @link http://publicsuffix.org/learn/ + */ + public function usePublicSuffixList($useList) + { + $this->useList = (bool)$useList; + } + + /** + * Returns string representation of object + * + * @return string + * + * @see Serializable::serialize() + */ + public function serialize() + { + $cookies = $this->getAll(); + if (!$this->serializeSession) { + for ($i = count($cookies) - 1; $i >= 0; $i--) { + if (empty($cookies[$i]['expires'])) { + unset($cookies[$i]); + } + } + } + return serialize(array( + 'cookies' => $cookies, + 'serializeSession' => $this->serializeSession, + 'useList' => $this->useList, + 'ignoreInvalid' => $this->ignoreInvalid + )); + } + + /** + * Constructs the object from serialized string + * + * @param string $serialized string representation + * + * @see Serializable::unserialize() + */ + public function unserialize($serialized) + { + $data = unserialize($serialized); + $now = $this->now(); + $this->serializeSessionCookies($data['serializeSession']); + $this->usePublicSuffixList($data['useList']); + if (array_key_exists('ignoreInvalid', $data)) { + $this->ignoreInvalidCookies($data['ignoreInvalid']); + } + foreach ($data['cookies'] as $cookie) { + if (!empty($cookie['expires']) && $cookie['expires'] <= $now) { + continue; + } + if (!isset($this->cookies[$cookie['domain']])) { + $this->cookies[$cookie['domain']] = array(); + } + if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { + $this->cookies[$cookie['domain']][$cookie['path']] = array(); + } + $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; + } + } + + /** + * Checks whether a cookie domain matches a request host. + * + * The method is used by {@link store()} to check for whether a document + * at given URL can set a cookie with a given domain attribute and by + * {@link getMatching()} to find cookies matching the request URL. + * + * @param string $requestHost request host + * @param string $cookieDomain cookie domain + * + * @return bool match success + */ + public function domainMatch($requestHost, $cookieDomain) + { + if ($requestHost == $cookieDomain) { + return true; + } + // IP address, we require exact match + if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) { + return false; + } + if ('.' != $cookieDomain[0]) { + $cookieDomain = '.' . $cookieDomain; + } + // prevents setting cookies for '.com' and similar domains + if (!$this->useList && substr_count($cookieDomain, '.') < 2 + || $this->useList && !self::getRegisteredDomain($cookieDomain) + ) { + return false; + } + return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain; + } + + /** + * Removes subdomains to get the registered domain (the first after top-level) + * + * The method will check Public Suffix List to find out where top-level + * domain ends and registered domain starts. It will remove domain parts + * to the left of registered one. + * + * @param string $domain domain name + * + * @return string|bool registered domain, will return false if $domain is + * either invalid or a TLD itself + */ + public static function getRegisteredDomain($domain) + { + $domainParts = explode('.', ltrim($domain, '.')); + + // load the list if needed + if (empty(self::$psl)) { + $path = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTTP_Request2'; + if (0 === strpos($path, '@' . 'data_dir@')) { + $path = realpath( + dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' + ); + } + self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php'; + } + + if (!($result = self::checkDomainsList($domainParts, self::$psl))) { + // known TLD, invalid domain name + return false; + } + + // unknown TLD + if (!strpos($result, '.')) { + // fallback to checking that domain "has at least two dots" + if (2 > ($count = count($domainParts))) { + return false; + } + return $domainParts[$count - 2] . '.' . $domainParts[$count - 1]; + } + return $result; + } + + /** + * Recursive helper method for {@link getRegisteredDomain()} + * + * @param array $domainParts remaining domain parts + * @param mixed $listNode node in {@link HTTP_Request2_CookieJar::$psl} to check + * + * @return string|null concatenated domain parts, null in case of error + */ + protected static function checkDomainsList(array $domainParts, $listNode) + { + $sub = array_pop($domainParts); + $result = null; + + if (!is_array($listNode) || is_null($sub) + || array_key_exists('!' . $sub, $listNode) + ) { + return $sub; + + } elseif (array_key_exists($sub, $listNode)) { + $result = self::checkDomainsList($domainParts, $listNode[$sub]); + + } elseif (array_key_exists('*', $listNode)) { + $result = self::checkDomainsList($domainParts, $listNode['*']); + + } else { + return $sub; + } + + return (strlen($result) > 0) ? ($result . '.' . $sub) : null; + } +} ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/Exception.php b/bundled-libs/HTTP/Request2/Exception.php index 5fcd6917..de9432df 100644 --- a/bundled-libs/HTTP/Request2/Exception.php +++ b/bundled-libs/HTTP/Request2/Exception.php @@ -1,160 +1,160 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Base class for exceptions in PEAR - */ -require_once 'PEAR/Exception.php'; - -/** - * Base exception class for HTTP_Request2 package - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132 - */ -class HTTP_Request2_Exception extends PEAR_Exception -{ - /** An invalid argument was passed to a method */ - const INVALID_ARGUMENT = 1; - /** Some required value was not available */ - const MISSING_VALUE = 2; - /** Request cannot be processed due to errors in PHP configuration */ - const MISCONFIGURATION = 3; - /** Error reading the local file */ - const READ_ERROR = 4; - - /** Server returned a response that does not conform to HTTP protocol */ - const MALFORMED_RESPONSE = 10; - /** Failure decoding Content-Encoding or Transfer-Encoding of response */ - const DECODE_ERROR = 20; - /** Operation timed out */ - const TIMEOUT = 30; - /** Number of redirects exceeded 'max_redirects' configuration parameter */ - const TOO_MANY_REDIRECTS = 40; - /** Redirect to a protocol other than http(s):// */ - const NON_HTTP_REDIRECT = 50; - - /** - * Native error code - * @var int - */ - private $_nativeCode; - - /** - * Constructor, can set package error code and native error code - * - * @param string $message exception message - * @param int $code package error code, one of class constants - * @param int $nativeCode error code from underlying PHP extension - */ - public function __construct($message = null, $code = null, $nativeCode = null) - { - parent::__construct($message, $code); - $this->_nativeCode = $nativeCode; - } - - /** - * Returns error code produced by underlying PHP extension - * - * For Socket Adapter this may contain error number returned by - * stream_socket_client(), for Curl Adapter this will contain error number - * returned by curl_errno() - * - * @return integer - */ - public function getNativeCode() - { - return $this->_nativeCode; - } -} - -/** - * Exception thrown in case of missing features - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception -{ -} - -/** - * Exception that represents error in the program logic - * - * This exception usually implies a programmer's error, like passing invalid - * data to methods or trying to use PHP extensions that weren't installed or - * enabled. Usually exceptions of this kind will be thrown before request even - * starts. - * - * The exception will usually contain a package error code. - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_LogicException extends HTTP_Request2_Exception -{ -} - -/** - * Exception thrown when connection to a web or proxy server fails - * - * The exception will not contain a package error code, but will contain - * native error code, as returned by stream_socket_client() or curl_errno(). - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception -{ -} - -/** - * Exception thrown when sending or receiving HTTP message fails - * - * The exception may contain both package error code and native error code. - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_MessageException extends HTTP_Request2_Exception -{ -} + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for exceptions in PEAR + */ +require_once 'PEAR/Exception.php'; + +/** + * Base exception class for HTTP_Request2 package + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132 + */ +class HTTP_Request2_Exception extends PEAR_Exception +{ + /** An invalid argument was passed to a method */ + const INVALID_ARGUMENT = 1; + /** Some required value was not available */ + const MISSING_VALUE = 2; + /** Request cannot be processed due to errors in PHP configuration */ + const MISCONFIGURATION = 3; + /** Error reading the local file */ + const READ_ERROR = 4; + + /** Server returned a response that does not conform to HTTP protocol */ + const MALFORMED_RESPONSE = 10; + /** Failure decoding Content-Encoding or Transfer-Encoding of response */ + const DECODE_ERROR = 20; + /** Operation timed out */ + const TIMEOUT = 30; + /** Number of redirects exceeded 'max_redirects' configuration parameter */ + const TOO_MANY_REDIRECTS = 40; + /** Redirect to a protocol other than http(s):// */ + const NON_HTTP_REDIRECT = 50; + + /** + * Native error code + * @var int + */ + private $_nativeCode; + + /** + * Constructor, can set package error code and native error code + * + * @param string $message exception message + * @param int $code package error code, one of class constants + * @param int $nativeCode error code from underlying PHP extension + */ + public function __construct($message = null, $code = null, $nativeCode = null) + { + parent::__construct($message, $code); + $this->_nativeCode = $nativeCode; + } + + /** + * Returns error code produced by underlying PHP extension + * + * For Socket Adapter this may contain error number returned by + * stream_socket_client(), for Curl Adapter this will contain error number + * returned by curl_errno() + * + * @return integer + */ + public function getNativeCode() + { + return $this->_nativeCode; + } +} + +/** + * Exception thrown in case of missing features + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception +{ +} + +/** + * Exception that represents error in the program logic + * + * This exception usually implies a programmer's error, like passing invalid + * data to methods or trying to use PHP extensions that weren't installed or + * enabled. Usually exceptions of this kind will be thrown before request even + * starts. + * + * The exception will usually contain a package error code. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_LogicException extends HTTP_Request2_Exception +{ +} + +/** + * Exception thrown when connection to a web or proxy server fails + * + * The exception will not contain a package error code, but will contain + * native error code, as returned by stream_socket_client() or curl_errno(). + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception +{ +} + +/** + * Exception thrown when sending or receiving HTTP message fails + * + * The exception may contain both package error code and native error code. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_MessageException extends HTTP_Request2_Exception +{ +} ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/MultipartBody.php b/bundled-libs/HTTP/Request2/MultipartBody.php index 198f563b..e5a38459 100644 --- a/bundled-libs/HTTP/Request2/MultipartBody.php +++ b/bundled-libs/HTTP/Request2/MultipartBody.php @@ -1,268 +1,268 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** Exception class for HTTP_Request2 package */ -require_once 'HTTP/Request2/Exception.php'; - -/** - * Class for building multipart/form-data request body - * - * The class helps to reduce memory consumption by streaming large file uploads - * from disk, it also allows monitoring of upload progress (see request #7630) - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://tools.ietf.org/html/rfc1867 - */ -class HTTP_Request2_MultipartBody -{ - /** - * MIME boundary - * @var string - */ - private $_boundary; - - /** - * Form parameters added via {@link HTTP_Request2::addPostParameter()} - * @var array - */ - private $_params = array(); - - /** - * File uploads added via {@link HTTP_Request2::addUpload()} - * @var array - */ - private $_uploads = array(); - - /** - * Header for parts with parameters - * @var string - */ - private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n"; - - /** - * Header for parts with uploads - * @var string - */ - private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n"; - - /** - * Current position in parameter and upload arrays - * - * First number is index of "current" part, second number is position within - * "current" part - * - * @var array - */ - private $_pos = array(0, 0); - - - /** - * Constructor. Sets the arrays with POST data. - * - * @param array $params values of form fields set via - * {@link HTTP_Request2::addPostParameter()} - * @param array $uploads file uploads set via - * {@link HTTP_Request2::addUpload()} - * @param bool $useBrackets whether to append brackets to array variable names - */ - public function __construct(array $params, array $uploads, $useBrackets = true) - { - $this->_params = self::_flattenArray('', $params, $useBrackets); - foreach ($uploads as $fieldName => $f) { - if (!is_array($f['fp'])) { - $this->_uploads[] = $f + array('name' => $fieldName); - } else { - for ($i = 0; $i < count($f['fp']); $i++) { - $upload = array( - 'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName) - ); - foreach (array('fp', 'filename', 'size', 'type') as $key) { - $upload[$key] = $f[$key][$i]; - } - $this->_uploads[] = $upload; - } - } - } - } - - /** - * Returns the length of the body to use in Content-Length header - * - * @return integer - */ - public function getLength() - { - $boundaryLength = strlen($this->getBoundary()); - $headerParamLength = strlen($this->_headerParam) - 4 + $boundaryLength; - $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength; - $length = $boundaryLength + 6; - foreach ($this->_params as $p) { - $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2; - } - foreach ($this->_uploads as $u) { - $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) + - strlen($u['filename']) + $u['size'] + 2; - } - return $length; - } - - /** - * Returns the boundary to use in Content-Type header - * - * @return string - */ - public function getBoundary() - { - if (empty($this->_boundary)) { - $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime()); - } - return $this->_boundary; - } - - /** - * Returns next chunk of request body - * - * @param integer $length Number of bytes to read - * - * @return string Up to $length bytes of data, empty string if at end - * @throws HTTP_Request2_LogicException - */ - public function read($length) - { - $ret = ''; - $boundary = $this->getBoundary(); - $paramCount = count($this->_params); - $uploadCount = count($this->_uploads); - while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) { - $oldLength = $length; - if ($this->_pos[0] < $paramCount) { - $param = sprintf( - $this->_headerParam, $boundary, $this->_params[$this->_pos[0]][0] - ) . $this->_params[$this->_pos[0]][1] . "\r\n"; - $ret .= substr($param, $this->_pos[1], $length); - $length -= min(strlen($param) - $this->_pos[1], $length); - - } elseif ($this->_pos[0] < $paramCount + $uploadCount) { - $pos = $this->_pos[0] - $paramCount; - $header = sprintf( - $this->_headerUpload, $boundary, $this->_uploads[$pos]['name'], - $this->_uploads[$pos]['filename'], $this->_uploads[$pos]['type'] - ); - if ($this->_pos[1] < strlen($header)) { - $ret .= substr($header, $this->_pos[1], $length); - $length -= min(strlen($header) - $this->_pos[1], $length); - } - $filePos = max(0, $this->_pos[1] - strlen($header)); - if ($filePos < $this->_uploads[$pos]['size']) { - while ($length > 0 && !feof($this->_uploads[$pos]['fp'])) { - if (false === ($chunk = fread($this->_uploads[$pos]['fp'], $length))) { - throw new HTTP_Request2_LogicException( - 'Failed reading file upload', HTTP_Request2_Exception::READ_ERROR - ); - } - $ret .= $chunk; - $length -= strlen($chunk); - } - } - if ($length > 0) { - $start = $this->_pos[1] + ($oldLength - $length) - - strlen($header) - $this->_uploads[$pos]['size']; - $ret .= substr("\r\n", $start, $length); - $length -= min(2 - $start, $length); - } - - } else { - $closing = '--' . $boundary . "--\r\n"; - $ret .= substr($closing, $this->_pos[1], $length); - $length -= min(strlen($closing) - $this->_pos[1], $length); - } - if ($length > 0) { - $this->_pos = array($this->_pos[0] + 1, 0); - } else { - $this->_pos[1] += $oldLength; - } - } - return $ret; - } - - /** - * Sets the current position to the start of the body - * - * This allows reusing the same body in another request - */ - public function rewind() - { - $this->_pos = array(0, 0); - foreach ($this->_uploads as $u) { - rewind($u['fp']); - } - } - - /** - * Returns the body as string - * - * Note that it reads all file uploads into memory so it is a good idea not - * to use this method with large file uploads and rely on read() instead. - * - * @return string - */ - public function __toString() - { - $this->rewind(); - return $this->read($this->getLength()); - } - - - /** - * Helper function to change the (probably multidimensional) associative array - * into the simple one. - * - * @param string $name name for item - * @param mixed $values item's values - * @param bool $useBrackets whether to append [] to array variables' names - * - * @return array array with the following items: array('item name', 'item value'); - */ - private static function _flattenArray($name, $values, $useBrackets) - { - if (!is_array($values)) { - return array(array($name, $values)); - } else { - $ret = array(); - foreach ($values as $k => $v) { - if (empty($name)) { - $newName = $k; - } elseif ($useBrackets) { - $newName = $name . '[' . $k . ']'; - } else { - $newName = $name; - } - $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets)); - } - return $ret; - } - } -} -?> + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Exception class for HTTP_Request2 package */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Class for building multipart/form-data request body + * + * The class helps to reduce memory consumption by streaming large file uploads + * from disk, it also allows monitoring of upload progress (see request #7630) + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://tools.ietf.org/html/rfc1867 + */ +class HTTP_Request2_MultipartBody +{ + /** + * MIME boundary + * @var string + */ + private $_boundary; + + /** + * Form parameters added via {@link HTTP_Request2::addPostParameter()} + * @var array + */ + private $_params = array(); + + /** + * File uploads added via {@link HTTP_Request2::addUpload()} + * @var array + */ + private $_uploads = array(); + + /** + * Header for parts with parameters + * @var string + */ + private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n"; + + /** + * Header for parts with uploads + * @var string + */ + private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n"; + + /** + * Current position in parameter and upload arrays + * + * First number is index of "current" part, second number is position within + * "current" part + * + * @var array + */ + private $_pos = array(0, 0); + + + /** + * Constructor. Sets the arrays with POST data. + * + * @param array $params values of form fields set via + * {@link HTTP_Request2::addPostParameter()} + * @param array $uploads file uploads set via + * {@link HTTP_Request2::addUpload()} + * @param bool $useBrackets whether to append brackets to array variable names + */ + public function __construct(array $params, array $uploads, $useBrackets = true) + { + $this->_params = self::_flattenArray('', $params, $useBrackets); + foreach ($uploads as $fieldName => $f) { + if (!is_array($f['fp'])) { + $this->_uploads[] = $f + array('name' => $fieldName); + } else { + for ($i = 0; $i < count($f['fp']); $i++) { + $upload = array( + 'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName) + ); + foreach (array('fp', 'filename', 'size', 'type') as $key) { + $upload[$key] = $f[$key][$i]; + } + $this->_uploads[] = $upload; + } + } + } + } + + /** + * Returns the length of the body to use in Content-Length header + * + * @return integer + */ + public function getLength() + { + $boundaryLength = strlen($this->getBoundary()); + $headerParamLength = strlen($this->_headerParam) - 4 + $boundaryLength; + $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength; + $length = $boundaryLength + 6; + foreach ($this->_params as $p) { + $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2; + } + foreach ($this->_uploads as $u) { + $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) + + strlen($u['filename']) + $u['size'] + 2; + } + return $length; + } + + /** + * Returns the boundary to use in Content-Type header + * + * @return string + */ + public function getBoundary() + { + if (empty($this->_boundary)) { + $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime()); + } + return $this->_boundary; + } + + /** + * Returns next chunk of request body + * + * @param integer $length Number of bytes to read + * + * @return string Up to $length bytes of data, empty string if at end + * @throws HTTP_Request2_LogicException + */ + public function read($length) + { + $ret = ''; + $boundary = $this->getBoundary(); + $paramCount = count($this->_params); + $uploadCount = count($this->_uploads); + while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) { + $oldLength = $length; + if ($this->_pos[0] < $paramCount) { + $param = sprintf( + $this->_headerParam, $boundary, $this->_params[$this->_pos[0]][0] + ) . $this->_params[$this->_pos[0]][1] . "\r\n"; + $ret .= substr($param, $this->_pos[1], $length); + $length -= min(strlen($param) - $this->_pos[1], $length); + + } elseif ($this->_pos[0] < $paramCount + $uploadCount) { + $pos = $this->_pos[0] - $paramCount; + $header = sprintf( + $this->_headerUpload, $boundary, $this->_uploads[$pos]['name'], + $this->_uploads[$pos]['filename'], $this->_uploads[$pos]['type'] + ); + if ($this->_pos[1] < strlen($header)) { + $ret .= substr($header, $this->_pos[1], $length); + $length -= min(strlen($header) - $this->_pos[1], $length); + } + $filePos = max(0, $this->_pos[1] - strlen($header)); + if ($filePos < $this->_uploads[$pos]['size']) { + while ($length > 0 && !feof($this->_uploads[$pos]['fp'])) { + if (false === ($chunk = fread($this->_uploads[$pos]['fp'], $length))) { + throw new HTTP_Request2_LogicException( + 'Failed reading file upload', HTTP_Request2_Exception::READ_ERROR + ); + } + $ret .= $chunk; + $length -= strlen($chunk); + } + } + if ($length > 0) { + $start = $this->_pos[1] + ($oldLength - $length) - + strlen($header) - $this->_uploads[$pos]['size']; + $ret .= substr("\r\n", $start, $length); + $length -= min(2 - $start, $length); + } + + } else { + $closing = '--' . $boundary . "--\r\n"; + $ret .= substr($closing, $this->_pos[1], $length); + $length -= min(strlen($closing) - $this->_pos[1], $length); + } + if ($length > 0) { + $this->_pos = array($this->_pos[0] + 1, 0); + } else { + $this->_pos[1] += $oldLength; + } + } + return $ret; + } + + /** + * Sets the current position to the start of the body + * + * This allows reusing the same body in another request + */ + public function rewind() + { + $this->_pos = array(0, 0); + foreach ($this->_uploads as $u) { + rewind($u['fp']); + } + } + + /** + * Returns the body as string + * + * Note that it reads all file uploads into memory so it is a good idea not + * to use this method with large file uploads and rely on read() instead. + * + * @return string + */ + public function __toString() + { + $this->rewind(); + return $this->read($this->getLength()); + } + + + /** + * Helper function to change the (probably multidimensional) associative array + * into the simple one. + * + * @param string $name name for item + * @param mixed $values item's values + * @param bool $useBrackets whether to append [] to array variables' names + * + * @return array array with the following items: array('item name', 'item value'); + */ + private static function _flattenArray($name, $values, $useBrackets) + { + if (!is_array($values)) { + return array(array($name, $values)); + } else { + $ret = array(); + foreach ($values as $k => $v) { + if (empty($name)) { + $newName = $k; + } elseif ($useBrackets) { + $newName = $name . '[' . $k . ']'; + } else { + $newName = $name; + } + $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets)); + } + return $ret; + } + } +} +?> diff --git a/bundled-libs/HTTP/Request2/Observer/Log.php b/bundled-libs/HTTP/Request2/Observer/Log.php index 96733de5..069baf8e 100644 --- a/bundled-libs/HTTP/Request2/Observer/Log.php +++ b/bundled-libs/HTTP/Request2/Observer/Log.php @@ -1,192 +1,192 @@ - - * @author Alexey Borzov - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Exception class for HTTP_Request2 package - */ -require_once 'HTTP/Request2/Exception.php'; - -/** - * A debug observer useful for debugging / testing. - * - * This observer logs to a log target data corresponding to the various request - * and response events, it logs by default to php://output but can be configured - * to log to a file or via the PEAR Log package. - * - * A simple example: - * - * require_once 'HTTP/Request2.php'; - * require_once 'HTTP/Request2/Observer/Log.php'; - * - * $request = new HTTP_Request2('http://www.example.com'); - * $observer = new HTTP_Request2_Observer_Log(); - * $request->attach($observer); - * $request->send(); - * - * - * A more complex example with PEAR Log: - * - * require_once 'HTTP/Request2.php'; - * require_once 'HTTP/Request2/Observer/Log.php'; - * require_once 'Log.php'; - * - * $request = new HTTP_Request2('http://www.example.com'); - * // we want to log with PEAR log - * $observer = new HTTP_Request2_Observer_Log(Log::factory('console')); - * - * // we only want to log received headers - * $observer->events = array('receivedHeaders'); - * - * $request->attach($observer); - * $request->send(); - * - * - * @category HTTP - * @package HTTP_Request2 - * @author David Jean Louis - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - */ -class HTTP_Request2_Observer_Log implements SplObserver -{ - // properties {{{ - - /** - * The log target, it can be a a resource or a PEAR Log instance. - * - * @var resource|Log $target - */ - protected $target = null; - - /** - * The events to log. - * - * @var array $events - */ - public $events = array( - 'connect', - 'sentHeaders', - 'sentBody', - 'receivedHeaders', - 'receivedBody', - 'disconnect', - ); - - // }}} - // __construct() {{{ - - /** - * Constructor. - * - * @param mixed $target Can be a file path (default: php://output), a resource, - * or an instance of the PEAR Log class. - * @param array $events Array of events to listen to (default: all events) - * - * @return void - */ - public function __construct($target = 'php://output', array $events = array()) - { - if (!empty($events)) { - $this->events = $events; - } - if (is_resource($target) || $target instanceof Log) { - $this->target = $target; - } elseif (false === ($this->target = @fopen($target, 'ab'))) { - throw new HTTP_Request2_Exception("Unable to open '{$target}'"); - } - } - - // }}} - // update() {{{ - - /** - * Called when the request notifies us of an event. - * - * @param HTTP_Request2 $subject The HTTP_Request2 instance - * - * @return void - */ - public function update(SplSubject $subject) - { - $event = $subject->getLastEvent(); - if (!in_array($event['name'], $this->events)) { - return; - } - - switch ($event['name']) { - case 'connect': - $this->log('* Connected to ' . $event['data']); - break; - case 'sentHeaders': - $headers = explode("\r\n", $event['data']); - array_pop($headers); - foreach ($headers as $header) { - $this->log('> ' . $header); - } - break; - case 'sentBody': - $this->log('> ' . $event['data'] . ' byte(s) sent'); - break; - case 'receivedHeaders': - $this->log(sprintf( - '< HTTP/%s %s %s', $event['data']->getVersion(), - $event['data']->getStatus(), $event['data']->getReasonPhrase() - )); - $headers = $event['data']->getHeader(); - foreach ($headers as $key => $val) { - $this->log('< ' . $key . ': ' . $val); - } - $this->log('< '); - break; - case 'receivedBody': - $this->log($event['data']->getBody()); - break; - case 'disconnect': - $this->log('* Disconnected'); - break; - } - } - - // }}} - // log() {{{ - - /** - * Logs the given message to the configured target. - * - * @param string $message Message to display - * - * @return void - */ - protected function log($message) - { - if ($this->target instanceof Log) { - $this->target->debug($message); - } elseif (is_resource($this->target)) { - fwrite($this->target, $message . "\r\n"); - } - } - - // }}} -} - + + * @author Alexey Borzov + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * A debug observer useful for debugging / testing. + * + * This observer logs to a log target data corresponding to the various request + * and response events, it logs by default to php://output but can be configured + * to log to a file or via the PEAR Log package. + * + * A simple example: + * + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/Log.php'; + * + * $request = new HTTP_Request2('http://www.example.com'); + * $observer = new HTTP_Request2_Observer_Log(); + * $request->attach($observer); + * $request->send(); + * + * + * A more complex example with PEAR Log: + * + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/Log.php'; + * require_once 'Log.php'; + * + * $request = new HTTP_Request2('http://www.example.com'); + * // we want to log with PEAR log + * $observer = new HTTP_Request2_Observer_Log(Log::factory('console')); + * + * // we only want to log received headers + * $observer->events = array('receivedHeaders'); + * + * $request->attach($observer); + * $request->send(); + * + * + * @category HTTP + * @package HTTP_Request2 + * @author David Jean Louis + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Observer_Log implements SplObserver +{ + // properties {{{ + + /** + * The log target, it can be a a resource or a PEAR Log instance. + * + * @var resource|Log $target + */ + protected $target = null; + + /** + * The events to log. + * + * @var array $events + */ + public $events = array( + 'connect', + 'sentHeaders', + 'sentBody', + 'receivedHeaders', + 'receivedBody', + 'disconnect', + ); + + // }}} + // __construct() {{{ + + /** + * Constructor. + * + * @param mixed $target Can be a file path (default: php://output), a resource, + * or an instance of the PEAR Log class. + * @param array $events Array of events to listen to (default: all events) + * + * @return void + */ + public function __construct($target = 'php://output', array $events = array()) + { + if (!empty($events)) { + $this->events = $events; + } + if (is_resource($target) || $target instanceof Log) { + $this->target = $target; + } elseif (false === ($this->target = @fopen($target, 'ab'))) { + throw new HTTP_Request2_Exception("Unable to open '{$target}'"); + } + } + + // }}} + // update() {{{ + + /** + * Called when the request notifies us of an event. + * + * @param HTTP_Request2 $subject The HTTP_Request2 instance + * + * @return void + */ + public function update(SplSubject $subject) + { + $event = $subject->getLastEvent(); + if (!in_array($event['name'], $this->events)) { + return; + } + + switch ($event['name']) { + case 'connect': + $this->log('* Connected to ' . $event['data']); + break; + case 'sentHeaders': + $headers = explode("\r\n", $event['data']); + array_pop($headers); + foreach ($headers as $header) { + $this->log('> ' . $header); + } + break; + case 'sentBody': + $this->log('> ' . $event['data'] . ' byte(s) sent'); + break; + case 'receivedHeaders': + $this->log(sprintf( + '< HTTP/%s %s %s', $event['data']->getVersion(), + $event['data']->getStatus(), $event['data']->getReasonPhrase() + )); + $headers = $event['data']->getHeader(); + foreach ($headers as $key => $val) { + $this->log('< ' . $key . ': ' . $val); + } + $this->log('< '); + break; + case 'receivedBody': + $this->log($event['data']->getBody()); + break; + case 'disconnect': + $this->log('* Disconnected'); + break; + } + } + + // }}} + // log() {{{ + + /** + * Logs the given message to the configured target. + * + * @param string $message Message to display + * + * @return void + */ + protected function log($message) + { + if ($this->target instanceof Log) { + $this->target->debug($message); + } elseif (is_resource($this->target)) { + fwrite($this->target, $message . "\r\n"); + } + } + + // }}} +} + ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/Observer/UncompressingDownload.php b/bundled-libs/HTTP/Request2/Observer/UncompressingDownload.php new file mode 100644 index 00000000..8a3430a0 --- /dev/null +++ b/bundled-libs/HTTP/Request2/Observer/UncompressingDownload.php @@ -0,0 +1,265 @@ + + * @author Alexey Borzov + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +require_once 'HTTP/Request2/Response.php'; + +/** + * An observer that saves response body to stream, possibly uncompressing it + * + * This Observer is written in compliment to pear's HTTP_Request2 in order to + * avoid reading the whole response body in memory. Instead it writes the body + * to a stream. If the body is transferred with content-encoding set to + * "deflate" or "gzip" it is decoded on the fly. + * + * The constructor accepts an already opened (for write) stream (file_descriptor). + * If the response is deflate/gzip encoded a "zlib.inflate" filter is applied + * to the stream. When the body has been read from the request and written to + * the stream ("receivedBody" event) the filter is removed from the stream. + * + * The "zlib.inflate" filter works fine with pure "deflate" encoding. It does + * not understand the "deflate+zlib" and "gzip" headers though, so they have to + * be removed prior to being passed to the stream. This is done in the "update" + * method. + * + * It is also possible to limit the size of written extracted bytes by passing + * "max_bytes" to the constructor. This is important because e.g. 1GB of + * zeroes take about a MB when compressed. + * + * Exceptions are being thrown if data could not be written to the stream or + * the written bytes have already exceeded the requested maximum. If the "gzip" + * header is malformed or could not be parsed an exception will be thrown too. + * + * Example usage follows: + * + * + * require_once 'HTTP/Request2.php'; + * require_once 'HTTP/Request2/Observer/UncompressingDownload.php'; + * + * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html'; + * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on'; + * $inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on&zlib=on'; + * #$outPath = "/dev/null"; + * $outPath = "delme"; + * + * $stream = fopen($outPath, 'wb'); + * if (!$stream) { + * throw new Exception('fopen failed'); + * } + * + * $request = new HTTP_Request2( + * $inPath, + * HTTP_Request2::METHOD_GET, + * array( + * 'store_body' => false, + * 'connect_timeout' => 5, + * 'timeout' => 10, + * 'ssl_verify_peer' => true, + * 'ssl_verify_host' => true, + * 'ssl_cafile' => null, + * 'ssl_capath' => '/etc/ssl/certs', + * 'max_redirects' => 10, + * 'follow_redirects' => true, + * 'strict_redirects' => false + * ) + * ); + * + * $observer = new HTTP_Request2_Observer_UncompressingDownload($stream, 9999999); + * $request->attach($observer); + * + * $response = $request->send(); + * + * fclose($stream); + * echo "OK\n"; + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Delian Krustev + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Observer_UncompressingDownload implements SplObserver +{ + /** + * The stream to write response body to + * @var resource + */ + private $_stream; + + /** + * zlib.inflate filter possibly added to stream + * @var resource + */ + private $_streamFilter; + + /** + * The value of response's Content-Encoding header + * @var string + */ + private $_encoding; + + /** + * Whether the observer is still waiting for gzip/deflate header + * @var bool + */ + private $_processingHeader = true; + + /** + * Starting position in the stream observer writes to + * @var int + */ + private $_startPosition = 0; + + /** + * Maximum bytes to write + * @var int|null + */ + private $_maxDownloadSize; + + /** + * Whether response being received is a redirect + * @var bool + */ + private $_redirect = false; + + /** + * Accumulated body chunks that may contain (gzip) header + * @var string + */ + private $_possibleHeader = ''; + + /** + * Class constructor + * + * Note that there might be problems with max_bytes and files bigger + * than 2 GB on 32bit platforms + * + * @param resource $stream a stream (or file descriptor) opened for writing. + * @param int $maxDownloadSize maximum bytes to write + */ + public function __construct($stream, $maxDownloadSize = null) + { + $this->_stream = $stream; + if ($maxDownloadSize) { + $this->_maxDownloadSize = $maxDownloadSize; + $this->_startPosition = ftell($this->_stream); + } + } + + /** + * Called when the request notifies us of an event. + * + * @param SplSubject $request The HTTP_Request2 instance + * + * @return void + * @throws HTTP_Request2_MessageException + */ + public function update(SplSubject $request) + { + /* @var $request HTTP_Request2 */ + $event = $request->getLastEvent(); + $encoded = false; + + /* @var $event['data'] HTTP_Request2_Response */ + switch ($event['name']) { + case 'receivedHeaders': + $this->_processingHeader = true; + $this->_redirect = $event['data']->isRedirect(); + $this->_encoding = strtolower($event['data']->getHeader('content-encoding')); + $this->_possibleHeader = ''; + break; + + case 'receivedEncodedBodyPart': + if (!$this->_streamFilter + && ($this->_encoding === 'deflate' || $this->_encoding === 'gzip') + ) { + $this->_streamFilter = stream_filter_append( + $this->_stream, 'zlib.inflate', STREAM_FILTER_WRITE + ); + } + $encoded = true; + // fall-through is intentional + + case 'receivedBodyPart': + if ($this->_redirect) { + break; + } + + if (!$encoded || !$this->_processingHeader) { + $bytes = fwrite($this->_stream, $event['data']); + + } else { + $offset = 0; + $this->_possibleHeader .= $event['data']; + if ('deflate' === $this->_encoding) { + if (2 > strlen($this->_possibleHeader)) { + break; + } + $header = unpack('n', substr($this->_possibleHeader, 0, 2)); + if (0 == $header[1] % 31) { + $offset = 2; + } + + } elseif ('gzip' === $this->_encoding) { + if (10 > strlen($this->_possibleHeader)) { + break; + } + try { + $offset = HTTP_Request2_Response::parseGzipHeader($this->_possibleHeader, false); + + } catch (HTTP_Request2_MessageException $e) { + // need more data? + if (false !== strpos($e->getMessage(), 'data too short')) { + break; + } + throw $e; + } + } + + $this->_processingHeader = false; + $bytes = fwrite($this->_stream, substr($this->_possibleHeader, $offset)); + } + + if (false === $bytes) { + throw new HTTP_Request2_MessageException('fwrite failed.'); + } + + if ($this->_maxDownloadSize + && ftell($this->_stream) - $this->_startPosition > $this->_maxDownloadSize + ) { + throw new HTTP_Request2_MessageException(sprintf( + 'Body length limit (%d bytes) reached', + $this->_maxDownloadSize + )); + } + break; + + case 'receivedBody': + if ($this->_streamFilter) { + stream_filter_remove($this->_streamFilter); + $this->_streamFilter = null; + } + break; + } + } +} diff --git a/bundled-libs/HTTP/Request2/Response.php b/bundled-libs/HTTP/Request2/Response.php index 7acbeaa3..b144fdae 100644 --- a/bundled-libs/HTTP/Request2/Response.php +++ b/bundled-libs/HTTP/Request2/Response.php @@ -1,631 +1,680 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** - * Exception class for HTTP_Request2 package - */ -require_once 'HTTP/Request2/Exception.php'; - -/** - * Class representing a HTTP response - * - * The class is designed to be used in "streaming" scenario, building the - * response as it is being received: - * - * $statusLine = read_status_line(); - * $response = new HTTP_Request2_Response($statusLine); - * do { - * $headerLine = read_header_line(); - * $response->parseHeaderLine($headerLine); - * } while ($headerLine != ''); - * - * while ($chunk = read_body()) { - * $response->appendBody($chunk); - * } - * - * var_dump($response->getHeader(), $response->getCookies(), $response->getBody()); - * - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://tools.ietf.org/html/rfc2616#section-6 - */ -class HTTP_Request2_Response -{ - /** - * HTTP protocol version (e.g. 1.0, 1.1) - * @var string - */ - protected $version; - - /** - * Status code - * @var integer - * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 - */ - protected $code; - - /** - * Reason phrase - * @var string - * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 - */ - protected $reasonPhrase; - - /** - * Effective URL (may be different from original request URL in case of redirects) - * @var string - */ - protected $effectiveUrl; - - /** - * Associative array of response headers - * @var array - */ - protected $headers = array(); - - /** - * Cookies set in the response - * @var array - */ - protected $cookies = array(); - - /** - * Name of last header processed by parseHederLine() - * - * Used to handle the headers that span multiple lines - * - * @var string - */ - protected $lastHeader = null; - - /** - * Response body - * @var string - */ - protected $body = ''; - - /** - * Whether the body is still encoded by Content-Encoding - * - * cURL provides the decoded body to the callback; if we are reading from - * socket the body is still gzipped / deflated - * - * @var bool - */ - protected $bodyEncoded; - - /** - * Associative array of HTTP status code / reason phrase. - * - * @var array - * @link http://tools.ietf.org/html/rfc2616#section-10 - */ - protected static $phrases = array( - - // 1xx: Informational - Request received, continuing process - 100 => 'Continue', - 101 => 'Switching Protocols', - - // 2xx: Success - The action was successfully received, understood and - // accepted - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - - // 3xx: Redirection - Further action must be taken in order to complete - // the request - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', // 1.1 - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 307 => 'Temporary Redirect', - - // 4xx: Client Error - The request contains bad syntax or cannot be - // fulfilled - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - - // 5xx: Server Error - The server failed to fulfill an apparently - // valid request - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported', - 509 => 'Bandwidth Limit Exceeded', - - ); - - /** - * Returns the default reason phrase for the given code or all reason phrases - * - * @param int $code Response code - * - * @return string|array|null Default reason phrase for $code if $code is given - * (null if no phrase is available), array of all - * reason phrases if $code is null - * @link http://pear.php.net/bugs/18716 - */ - public static function getDefaultReasonPhrase($code = null) - { - if (null === $code) { - return self::$phrases; - } else { - return isset(self::$phrases[$code]) ? self::$phrases[$code] : null; - } - } - - /** - * Constructor, parses the response status line - * - * @param string $statusLine Response status line (e.g. "HTTP/1.1 200 OK") - * @param bool $bodyEncoded Whether body is still encoded by Content-Encoding - * @param string $effectiveUrl Effective URL of the response - * - * @throws HTTP_Request2_MessageException if status line is invalid according to spec - */ - public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null) - { - if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) { - throw new HTTP_Request2_MessageException( - "Malformed response: {$statusLine}", - HTTP_Request2_Exception::MALFORMED_RESPONSE - ); - } - $this->version = $m[1]; - $this->code = intval($m[2]); - $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code); - $this->bodyEncoded = (bool)$bodyEncoded; - $this->effectiveUrl = (string)$effectiveUrl; - } - - /** - * Parses the line from HTTP response filling $headers array - * - * The method should be called after reading the line from socket or receiving - * it into cURL callback. Passing an empty string here indicates the end of - * response headers and triggers additional processing, so be sure to pass an - * empty string in the end. - * - * @param string $headerLine Line from HTTP response - */ - public function parseHeaderLine($headerLine) - { - $headerLine = trim($headerLine, "\r\n"); - - if ('' == $headerLine) { - // empty string signals the end of headers, process the received ones - if (!empty($this->headers['set-cookie'])) { - $cookies = is_array($this->headers['set-cookie'])? - $this->headers['set-cookie']: - array($this->headers['set-cookie']); - foreach ($cookies as $cookieString) { - $this->parseCookie($cookieString); - } - unset($this->headers['set-cookie']); - } - foreach (array_keys($this->headers) as $k) { - if (is_array($this->headers[$k])) { - $this->headers[$k] = implode(', ', $this->headers[$k]); - } - } - - } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) { - // string of the form header-name: header value - $name = strtolower($m[1]); - $value = trim($m[2]); - if (empty($this->headers[$name])) { - $this->headers[$name] = $value; - } else { - if (!is_array($this->headers[$name])) { - $this->headers[$name] = array($this->headers[$name]); - } - $this->headers[$name][] = $value; - } - $this->lastHeader = $name; - - } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) { - // continuation of a previous header - if (!is_array($this->headers[$this->lastHeader])) { - $this->headers[$this->lastHeader] .= ' ' . trim($m[1]); - } else { - $key = count($this->headers[$this->lastHeader]) - 1; - $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]); - } - } - } - - /** - * Parses a Set-Cookie header to fill $cookies array - * - * @param string $cookieString value of Set-Cookie header - * - * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html - */ - protected function parseCookie($cookieString) - { - $cookie = array( - 'expires' => null, - 'domain' => null, - 'path' => null, - 'secure' => false - ); - - if (!strpos($cookieString, ';')) { - // Only a name=value pair - $pos = strpos($cookieString, '='); - $cookie['name'] = trim(substr($cookieString, 0, $pos)); - $cookie['value'] = trim(substr($cookieString, $pos + 1)); - - } else { - // Some optional parameters are supplied - $elements = explode(';', $cookieString); - $pos = strpos($elements[0], '='); - $cookie['name'] = trim(substr($elements[0], 0, $pos)); - $cookie['value'] = trim(substr($elements[0], $pos + 1)); - - for ($i = 1; $i < count($elements); $i++) { - if (false === strpos($elements[$i], '=')) { - $elName = trim($elements[$i]); - $elValue = null; - } else { - list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); - } - $elName = strtolower($elName); - if ('secure' == $elName) { - $cookie['secure'] = true; - } elseif ('expires' == $elName) { - $cookie['expires'] = str_replace('"', '', $elValue); - } elseif ('path' == $elName || 'domain' == $elName) { - $cookie[$elName] = urldecode($elValue); - } else { - $cookie[$elName] = $elValue; - } - } - } - $this->cookies[] = $cookie; - } - - /** - * Appends a string to the response body - * - * @param string $bodyChunk part of response body - */ - public function appendBody($bodyChunk) - { - $this->body .= $bodyChunk; - } - - /** - * Returns the effective URL of the response - * - * This may be different from the request URL if redirects were followed. - * - * @return string - * @link http://pear.php.net/bugs/bug.php?id=18412 - */ - public function getEffectiveUrl() - { - return $this->effectiveUrl; - } - - /** - * Returns the status code - * - * @return integer - */ - public function getStatus() - { - return $this->code; - } - - /** - * Returns the reason phrase - * - * @return string - */ - public function getReasonPhrase() - { - return $this->reasonPhrase; - } - - /** - * Whether response is a redirect that can be automatically handled by HTTP_Request2 - * - * @return bool - */ - public function isRedirect() - { - return in_array($this->code, array(300, 301, 302, 303, 307)) - && isset($this->headers['location']); - } - - /** - * Returns either the named header or all response headers - * - * @param string $headerName Name of header to return - * - * @return string|array Value of $headerName header (null if header is - * not present), array of all response headers if - * $headerName is null - */ - public function getHeader($headerName = null) - { - if (null === $headerName) { - return $this->headers; - } else { - $headerName = strtolower($headerName); - return isset($this->headers[$headerName])? $this->headers[$headerName]: null; - } - } - - /** - * Returns cookies set in response - * - * @return array - */ - public function getCookies() - { - return $this->cookies; - } - - /** - * Returns the body of the response - * - * @return string - * @throws HTTP_Request2_Exception if body cannot be decoded - */ - public function getBody() - { - if (0 == strlen($this->body) || !$this->bodyEncoded - || !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate')) - ) { - return $this->body; - - } else { - if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { - $oldEncoding = mb_internal_encoding(); - mb_internal_encoding('8bit'); - } - - try { - switch (strtolower($this->getHeader('content-encoding'))) { - case 'gzip': - $decoded = self::decodeGzip($this->body); - break; - case 'deflate': - $decoded = self::decodeDeflate($this->body); - } - } catch (Exception $e) { - } - - if (!empty($oldEncoding)) { - mb_internal_encoding($oldEncoding); - } - if (!empty($e)) { - throw $e; - } - return $decoded; - } - } - - /** - * Get the HTTP version of the response - * - * @return string - */ - public function getVersion() - { - return $this->version; - } - - /** - * Decodes the message-body encoded by gzip - * - * The real decoding work is done by gzinflate() built-in function, this - * method only parses the header and checks data for compliance with - * RFC 1952 - * - * @param string $data gzip-encoded data - * - * @return string decoded data - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_MessageException - * @link http://tools.ietf.org/html/rfc1952 - */ - public static function decodeGzip($data) - { - $length = strlen($data); - // If it doesn't look like gzip-encoded data, don't bother - if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) { - return $data; - } - if (!function_exists('gzinflate')) { - throw new HTTP_Request2_LogicException( - 'Unable to decode body: gzip extension not available', - HTTP_Request2_Exception::MISCONFIGURATION - ); - } - $method = ord(substr($data, 2, 1)); - if (8 != $method) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: unknown compression method', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $flags = ord(substr($data, 3, 1)); - if ($flags & 224) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: reserved bits are set', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - - // header is 10 bytes minimum. may be longer, though. - $headerLength = 10; - // extra fields, need to skip 'em - if ($flags & 4) { - if ($length - $headerLength - 2 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $extraLength = unpack('v', substr($data, 10, 2)); - if ($length - $headerLength - 2 - $extraLength[1] < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $headerLength += $extraLength[1] + 2; - } - // file name, need to skip that - if ($flags & 8) { - if ($length - $headerLength - 1 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $filenameLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $headerLength += $filenameLength + 1; - } - // comment, need to skip that also - if ($flags & 16) { - if ($length - $headerLength - 1 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $commentLength = strpos(substr($data, $headerLength), chr(0)); - if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $headerLength += $commentLength + 1; - } - // have a CRC for header. let's check - if ($flags & 2) { - if ($length - $headerLength - 2 < 8) { - throw new HTTP_Request2_MessageException( - 'Error parsing gzip header: data too short', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); - $crcStored = unpack('v', substr($data, $headerLength, 2)); - if ($crcReal != $crcStored[1]) { - throw new HTTP_Request2_MessageException( - 'Header CRC check failed', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - $headerLength += 2; - } - // unpacked data CRC and size at the end of encoded data - $tmp = unpack('V2', substr($data, -8)); - $dataCrc = $tmp[1]; - $dataSize = $tmp[2]; - - // finally, call the gzinflate() function - // don't pass $dataSize to gzinflate, see bugs #13135, #14370 - $unpacked = gzinflate(substr($data, $headerLength, -8)); - if (false === $unpacked) { - throw new HTTP_Request2_MessageException( - 'gzinflate() call failed', - HTTP_Request2_Exception::DECODE_ERROR - ); - } elseif ($dataSize != strlen($unpacked)) { - throw new HTTP_Request2_MessageException( - 'Data size check failed', - HTTP_Request2_Exception::DECODE_ERROR - ); - } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { - throw new HTTP_Request2_Exception( - 'Data CRC check failed', - HTTP_Request2_Exception::DECODE_ERROR - ); - } - return $unpacked; - } - - /** - * Decodes the message-body encoded by deflate - * - * @param string $data deflate-encoded data - * - * @return string decoded data - * @throws HTTP_Request2_LogicException - */ - public static function decodeDeflate($data) - { - if (!function_exists('gzuncompress')) { - throw new HTTP_Request2_LogicException( - 'Unable to decode body: gzip extension not available', - HTTP_Request2_Exception::MISCONFIGURATION - ); - } - // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950, - // while many applications send raw deflate stream from RFC 1951. - // We should check for presence of zlib header and use gzuncompress() or - // gzinflate() as needed. See bug #15305 - $header = unpack('n', substr($data, 0, 2)); - return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data); - } -} + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Exception class for HTTP_Request2 package + */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Class representing a HTTP response + * + * The class is designed to be used in "streaming" scenario, building the + * response as it is being received: + * + * $statusLine = read_status_line(); + * $response = new HTTP_Request2_Response($statusLine); + * do { + * $headerLine = read_header_line(); + * $response->parseHeaderLine($headerLine); + * } while ($headerLine != ''); + * + * while ($chunk = read_body()) { + * $response->appendBody($chunk); + * } + * + * var_dump($response->getHeader(), $response->getCookies(), $response->getBody()); + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://tools.ietf.org/html/rfc2616#section-6 + */ +class HTTP_Request2_Response +{ + /** + * HTTP protocol version (e.g. 1.0, 1.1) + * @var string + */ + protected $version; + + /** + * Status code + * @var integer + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + */ + protected $code; + + /** + * Reason phrase + * @var string + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + */ + protected $reasonPhrase; + + /** + * Effective URL (may be different from original request URL in case of redirects) + * @var string + */ + protected $effectiveUrl; + + /** + * Associative array of response headers + * @var array + */ + protected $headers = array(); + + /** + * Cookies set in the response + * @var array + */ + protected $cookies = array(); + + /** + * Name of last header processed by parseHederLine() + * + * Used to handle the headers that span multiple lines + * + * @var string + */ + protected $lastHeader = null; + + /** + * Response body + * @var string + */ + protected $body = ''; + + /** + * Whether the body is still encoded by Content-Encoding + * + * cURL provides the decoded body to the callback; if we are reading from + * socket the body is still gzipped / deflated + * + * @var bool + */ + protected $bodyEncoded; + + /** + * Associative array of HTTP status code / reason phrase. + * + * @var array + * @link http://tools.ietf.org/html/rfc2616#section-10 + */ + protected static $phrases = array( + + // 1xx: Informational - Request received, continuing process + 100 => 'Continue', + 101 => 'Switching Protocols', + + // 2xx: Success - The action was successfully received, understood and + // accepted + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + + // 3xx: Redirection - Further action must be taken in order to complete + // the request + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // 1.1 + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + + // 4xx: Client Error - The request contains bad syntax or cannot be + // fulfilled + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + + // 5xx: Server Error - The server failed to fulfill an apparently + // valid request + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 509 => 'Bandwidth Limit Exceeded', + + ); + + /** + * Returns the default reason phrase for the given code or all reason phrases + * + * @param int $code Response code + * + * @return string|array|null Default reason phrase for $code if $code is given + * (null if no phrase is available), array of all + * reason phrases if $code is null + * @link http://pear.php.net/bugs/18716 + */ + public static function getDefaultReasonPhrase($code = null) + { + if (null === $code) { + return self::$phrases; + } else { + return isset(self::$phrases[$code]) ? self::$phrases[$code] : null; + } + } + + /** + * Constructor, parses the response status line + * + * @param string $statusLine Response status line (e.g. "HTTP/1.1 200 OK") + * @param bool $bodyEncoded Whether body is still encoded by Content-Encoding + * @param string $effectiveUrl Effective URL of the response + * + * @throws HTTP_Request2_MessageException if status line is invalid according to spec + */ + public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null) + { + if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) { + throw new HTTP_Request2_MessageException( + "Malformed response: {$statusLine}", + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } + $this->version = $m[1]; + $this->code = intval($m[2]); + $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code); + $this->bodyEncoded = (bool)$bodyEncoded; + $this->effectiveUrl = (string)$effectiveUrl; + } + + /** + * Parses the line from HTTP response filling $headers array + * + * The method should be called after reading the line from socket or receiving + * it into cURL callback. Passing an empty string here indicates the end of + * response headers and triggers additional processing, so be sure to pass an + * empty string in the end. + * + * @param string $headerLine Line from HTTP response + */ + public function parseHeaderLine($headerLine) + { + $headerLine = trim($headerLine, "\r\n"); + + if ('' == $headerLine) { + // empty string signals the end of headers, process the received ones + if (!empty($this->headers['set-cookie'])) { + $cookies = is_array($this->headers['set-cookie'])? + $this->headers['set-cookie']: + array($this->headers['set-cookie']); + foreach ($cookies as $cookieString) { + $this->parseCookie($cookieString); + } + unset($this->headers['set-cookie']); + } + foreach (array_keys($this->headers) as $k) { + if (is_array($this->headers[$k])) { + $this->headers[$k] = implode(', ', $this->headers[$k]); + } + } + + } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) { + // string of the form header-name: header value + $name = strtolower($m[1]); + $value = trim($m[2]); + if (empty($this->headers[$name])) { + $this->headers[$name] = $value; + } else { + if (!is_array($this->headers[$name])) { + $this->headers[$name] = array($this->headers[$name]); + } + $this->headers[$name][] = $value; + } + $this->lastHeader = $name; + + } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) { + // continuation of a previous header + if (!is_array($this->headers[$this->lastHeader])) { + $this->headers[$this->lastHeader] .= ' ' . trim($m[1]); + } else { + $key = count($this->headers[$this->lastHeader]) - 1; + $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]); + } + } + } + + /** + * Parses a Set-Cookie header to fill $cookies array + * + * @param string $cookieString value of Set-Cookie header + * + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + */ + protected function parseCookie($cookieString) + { + $cookie = array( + 'expires' => null, + 'domain' => null, + 'path' => null, + 'secure' => false + ); + + if (!strpos($cookieString, ';')) { + // Only a name=value pair + $pos = strpos($cookieString, '='); + $cookie['name'] = trim(substr($cookieString, 0, $pos)); + $cookie['value'] = trim(substr($cookieString, $pos + 1)); + + } else { + // Some optional parameters are supplied + $elements = explode(';', $cookieString); + $pos = strpos($elements[0], '='); + $cookie['name'] = trim(substr($elements[0], 0, $pos)); + $cookie['value'] = trim(substr($elements[0], $pos + 1)); + + for ($i = 1; $i < count($elements); $i++) { + if (false === strpos($elements[$i], '=')) { + $elName = trim($elements[$i]); + $elValue = null; + } else { + list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i])); + } + $elName = strtolower($elName); + if ('secure' == $elName) { + $cookie['secure'] = true; + } elseif ('expires' == $elName) { + $cookie['expires'] = str_replace('"', '', $elValue); + } elseif ('path' == $elName || 'domain' == $elName) { + $cookie[$elName] = urldecode($elValue); + } else { + $cookie[$elName] = $elValue; + } + } + } + $this->cookies[] = $cookie; + } + + /** + * Appends a string to the response body + * + * @param string $bodyChunk part of response body + */ + public function appendBody($bodyChunk) + { + $this->body .= $bodyChunk; + } + + /** + * Returns the effective URL of the response + * + * This may be different from the request URL if redirects were followed. + * + * @return string + * @link http://pear.php.net/bugs/bug.php?id=18412 + */ + public function getEffectiveUrl() + { + return $this->effectiveUrl; + } + + /** + * Returns the status code + * + * @return integer + */ + public function getStatus() + { + return $this->code; + } + + /** + * Returns the reason phrase + * + * @return string + */ + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** + * Whether response is a redirect that can be automatically handled by HTTP_Request2 + * + * @return bool + */ + public function isRedirect() + { + return in_array($this->code, array(300, 301, 302, 303, 307)) + && isset($this->headers['location']); + } + + /** + * Returns either the named header or all response headers + * + * @param string $headerName Name of header to return + * + * @return string|array Value of $headerName header (null if header is + * not present), array of all response headers if + * $headerName is null + */ + public function getHeader($headerName = null) + { + if (null === $headerName) { + return $this->headers; + } else { + $headerName = strtolower($headerName); + return isset($this->headers[$headerName])? $this->headers[$headerName]: null; + } + } + + /** + * Returns cookies set in response + * + * @return array + */ + public function getCookies() + { + return $this->cookies; + } + + /** + * Returns the body of the response + * + * @return string + * @throws HTTP_Request2_Exception if body cannot be decoded + */ + public function getBody() + { + if (0 == strlen($this->body) || !$this->bodyEncoded + || !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate')) + ) { + return $this->body; + + } else { + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + $oldEncoding = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + + try { + switch (strtolower($this->getHeader('content-encoding'))) { + case 'gzip': + $decoded = self::decodeGzip($this->body); + break; + case 'deflate': + $decoded = self::decodeDeflate($this->body); + } + } catch (Exception $e) { + } + + if (!empty($oldEncoding)) { + mb_internal_encoding($oldEncoding); + } + if (!empty($e)) { + throw $e; + } + return $decoded; + } + } + + /** + * Get the HTTP version of the response + * + * @return string + */ + public function getVersion() + { + return $this->version; + } + + /** + * Checks whether data starts with GZIP format identification bytes from RFC 1952 + * + * @param string $data gzip-encoded (presumably) data + * + * @return bool + */ + public static function hasGzipIdentification($data) + { + return 0 === strcmp(substr($data, 0, 2), "\x1f\x8b"); + } + + /** + * Tries to parse GZIP format header in the given string + * + * If the header conforms to RFC 1952, its length is returned. If any + * sanity check fails, HTTP_Request2_MessageException is thrown. + * + * Note: This function might be usable outside of HTTP_Request2 so it might + * be good idea to be moved to some common package. (Delian Krustev) + * + * @param string $data Either the complete response body or + * the leading part of it + * @param boolean $dataComplete Whether $data contains complete response body + * + * @return int gzip header length in bytes + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1952 + */ + public static function parseGzipHeader($data, $dataComplete = false) + { + // if data is complete, trailing 8 bytes should be present for size and crc32 + $length = strlen($data) - ($dataComplete ? 8 : 0); + + if ($length < 10 || !self::hasGzipIdentification($data)) { + throw new HTTP_Request2_MessageException( + 'The data does not seem to contain a valid gzip header', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + + $method = ord(substr($data, 2, 1)); + if (8 != $method) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: unknown compression method', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $flags = ord(substr($data, 3, 1)); + if ($flags & 224) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: reserved bits are set', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + + // header is 10 bytes minimum. may be longer, though. + $headerLength = 10; + // extra fields, need to skip 'em + if ($flags & 4) { + if ($length - $headerLength - 2 < 0) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $extraLength = unpack('v', substr($data, 10, 2)); + if ($length - $headerLength - 2 - $extraLength[1] < 0) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $headerLength += $extraLength[1] + 2; + } + // file name, need to skip that + if ($flags & 8) { + if ($length - $headerLength - 1 < 0) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $filenameLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $filenameLength + || $length - $headerLength - $filenameLength - 1 < 0 + ) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $headerLength += $filenameLength + 1; + } + // comment, need to skip that also + if ($flags & 16) { + if ($length - $headerLength - 1 < 0) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $commentLength = strpos(substr($data, $headerLength), chr(0)); + if (false === $commentLength + || $length - $headerLength - $commentLength - 1 < 0 + ) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $headerLength += $commentLength + 1; + } + // have a CRC for header. let's check + if ($flags & 2) { + if ($length - $headerLength - 2 < 0) { + throw new HTTP_Request2_MessageException( + 'Error parsing gzip header: data too short', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); + $crcStored = unpack('v', substr($data, $headerLength, 2)); + if ($crcReal != $crcStored[1]) { + throw new HTTP_Request2_MessageException( + 'Header CRC check failed', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + $headerLength += 2; + } + return $headerLength; + } + + /** + * Decodes the message-body encoded by gzip + * + * The real decoding work is done by gzinflate() built-in function, this + * method only parses the header and checks data for compliance with + * RFC 1952 + * + * @param string $data gzip-encoded data + * + * @return string decoded data + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1952 + */ + public static function decodeGzip($data) + { + // If it doesn't look like gzip-encoded data, don't bother + if (!self::hasGzipIdentification($data)) { + return $data; + } + if (!function_exists('gzinflate')) { + throw new HTTP_Request2_LogicException( + 'Unable to decode body: gzip extension not available', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + + // unpacked data CRC and size at the end of encoded data + $tmp = unpack('V2', substr($data, -8)); + $dataCrc = $tmp[1]; + $dataSize = $tmp[2]; + + $headerLength = self::parseGzipHeader($data, true); + + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 + $unpacked = gzinflate(substr($data, $headerLength, -8)); + if (false === $unpacked) { + throw new HTTP_Request2_MessageException( + 'gzinflate() call failed', + HTTP_Request2_Exception::DECODE_ERROR + ); + } elseif ($dataSize != strlen($unpacked)) { + throw new HTTP_Request2_MessageException( + 'Data size check failed', + HTTP_Request2_Exception::DECODE_ERROR + ); + } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { + throw new HTTP_Request2_MessageException( + 'Data CRC check failed', + HTTP_Request2_Exception::DECODE_ERROR + ); + } + return $unpacked; + } + + /** + * Decodes the message-body encoded by deflate + * + * @param string $data deflate-encoded data + * + * @return string decoded data + * @throws HTTP_Request2_LogicException + */ + public static function decodeDeflate($data) + { + if (!function_exists('gzuncompress')) { + throw new HTTP_Request2_LogicException( + 'Unable to decode body: gzip extension not available', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950, + // while many applications send raw deflate stream from RFC 1951. + // We should check for presence of zlib header and use gzuncompress() or + // gzinflate() as needed. See bug #15305 + $header = unpack('n', substr($data, 0, 2)); + return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data); + } +} ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/SOCKS5.php b/bundled-libs/HTTP/Request2/SOCKS5.php index 0b86445c..2a782ced 100644 --- a/bundled-libs/HTTP/Request2/SOCKS5.php +++ b/bundled-libs/HTTP/Request2/SOCKS5.php @@ -1,135 +1,135 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** Socket wrapper class used by Socket Adapter */ -require_once 'HTTP/Request2/SocketWrapper.php'; - -/** - * SOCKS5 proxy connection class (used by Socket Adapter) - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://pear.php.net/bugs/bug.php?id=19332 - * @link http://tools.ietf.org/html/rfc1928 - */ -class HTTP_Request2_SOCKS5 extends HTTP_Request2_SocketWrapper -{ - /** - * Constructor, tries to connect and authenticate to a SOCKS5 proxy - * - * @param string $address Proxy address, e.g. 'tcp://localhost:1080' - * @param int $timeout Connection timeout (seconds) - * @param array $contextOptions Stream context options - * @param string $username Proxy user name - * @param string $password Proxy password - * - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_ConnectionException - * @throws HTTP_Request2_MessageException - */ - public function __construct( - $address, $timeout = 10, array $contextOptions = array(), - $username = null, $password = null - ) { - parent::__construct($address, $timeout, $contextOptions); - - if (strlen($username)) { - $request = pack('C4', 5, 2, 0, 2); - } else { - $request = pack('C3', 5, 1, 0); - } - $this->write($request); - $response = unpack('Cversion/Cmethod', $this->read(3)); - if (5 != $response['version']) { - throw new HTTP_Request2_MessageException( - 'Invalid version received from SOCKS5 proxy: ' . $response['version'], - HTTP_Request2_Exception::MALFORMED_RESPONSE - ); - } - switch ($response['method']) { - case 2: - $this->performAuthentication($username, $password); - case 0: - break; - default: - throw new HTTP_Request2_ConnectionException( - "Connection rejected by proxy due to unsupported auth method" - ); - } - } - - /** - * Performs username/password authentication for SOCKS5 - * - * @param string $username Proxy user name - * @param string $password Proxy password - * - * @throws HTTP_Request2_ConnectionException - * @throws HTTP_Request2_MessageException - * @link http://tools.ietf.org/html/rfc1929 - */ - protected function performAuthentication($username, $password) - { - $request = pack('C2', 1, strlen($username)) . $username - . pack('C', strlen($password)) . $password; - - $this->write($request); - $response = unpack('Cvn/Cstatus', $this->read(3)); - if (1 != $response['vn'] || 0 != $response['status']) { - throw new HTTP_Request2_ConnectionException( - 'Connection rejected by proxy due to invalid username and/or password' - ); - } - } - - /** - * Connects to a remote host via proxy - * - * @param string $remoteHost Remote host - * @param int $remotePort Remote port - * - * @throws HTTP_Request2_ConnectionException - * @throws HTTP_Request2_MessageException - */ - public function connect($remoteHost, $remotePort) - { - $request = pack('C5', 0x05, 0x01, 0x00, 0x03, strlen($remoteHost)) - . $remoteHost . pack('n', $remotePort); - - $this->write($request); - $response = unpack('Cversion/Creply/Creserved', $this->read(1024)); - if (5 != $response['version'] || 0 != $response['reserved']) { - throw new HTTP_Request2_MessageException( - 'Invalid response received from SOCKS5 proxy', - HTTP_Request2_Exception::MALFORMED_RESPONSE - ); - } elseif (0 != $response['reply']) { - throw new HTTP_Request2_ConnectionException( - "Unable to connect to {$remoteHost}:{$remotePort} through SOCKS5 proxy", - 0, $response['reply'] - ); - } - } -} + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Socket wrapper class used by Socket Adapter */ +require_once 'HTTP/Request2/SocketWrapper.php'; + +/** + * SOCKS5 proxy connection class (used by Socket Adapter) + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://pear.php.net/bugs/bug.php?id=19332 + * @link http://tools.ietf.org/html/rfc1928 + */ +class HTTP_Request2_SOCKS5 extends HTTP_Request2_SocketWrapper +{ + /** + * Constructor, tries to connect and authenticate to a SOCKS5 proxy + * + * @param string $address Proxy address, e.g. 'tcp://localhost:1080' + * @param int $timeout Connection timeout (seconds) + * @param array $contextOptions Stream context options + * @param string $username Proxy user name + * @param string $password Proxy password + * + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + */ + public function __construct( + $address, $timeout = 10, array $contextOptions = array(), + $username = null, $password = null + ) { + parent::__construct($address, $timeout, $contextOptions); + + if (strlen($username)) { + $request = pack('C4', 5, 2, 0, 2); + } else { + $request = pack('C3', 5, 1, 0); + } + $this->write($request); + $response = unpack('Cversion/Cmethod', $this->read(3)); + if (5 != $response['version']) { + throw new HTTP_Request2_MessageException( + 'Invalid version received from SOCKS5 proxy: ' . $response['version'], + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } + switch ($response['method']) { + case 2: + $this->performAuthentication($username, $password); + case 0: + break; + default: + throw new HTTP_Request2_ConnectionException( + "Connection rejected by proxy due to unsupported auth method" + ); + } + } + + /** + * Performs username/password authentication for SOCKS5 + * + * @param string $username Proxy user name + * @param string $password Proxy password + * + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1929 + */ + protected function performAuthentication($username, $password) + { + $request = pack('C2', 1, strlen($username)) . $username + . pack('C', strlen($password)) . $password; + + $this->write($request); + $response = unpack('Cvn/Cstatus', $this->read(3)); + if (1 != $response['vn'] || 0 != $response['status']) { + throw new HTTP_Request2_ConnectionException( + 'Connection rejected by proxy due to invalid username and/or password' + ); + } + } + + /** + * Connects to a remote host via proxy + * + * @param string $remoteHost Remote host + * @param int $remotePort Remote port + * + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + */ + public function connect($remoteHost, $remotePort) + { + $request = pack('C5', 0x05, 0x01, 0x00, 0x03, strlen($remoteHost)) + . $remoteHost . pack('n', $remotePort); + + $this->write($request); + $response = unpack('Cversion/Creply/Creserved', $this->read(1024)); + if (5 != $response['version'] || 0 != $response['reserved']) { + throw new HTTP_Request2_MessageException( + 'Invalid response received from SOCKS5 proxy', + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } elseif (0 != $response['reply']) { + throw new HTTP_Request2_ConnectionException( + "Unable to connect to {$remoteHost}:{$remotePort} through SOCKS5 proxy", + 0, $response['reply'] + ); + } + } +} ?> \ No newline at end of file diff --git a/bundled-libs/HTTP/Request2/SocketWrapper.php b/bundled-libs/HTTP/Request2/SocketWrapper.php index 388c1b65..2cf4257d 100644 --- a/bundled-libs/HTTP/Request2/SocketWrapper.php +++ b/bundled-libs/HTTP/Request2/SocketWrapper.php @@ -1,297 +1,320 @@ - - * @copyright 2008-2014 Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @link http://pear.php.net/package/HTTP_Request2 - */ - -/** Exception classes for HTTP_Request2 package */ -require_once 'HTTP/Request2/Exception.php'; - -/** - * Socket wrapper class used by Socket Adapter - * - * Needed to properly handle connection errors, global timeout support and - * similar things. Loosely based on Net_Socket used by older HTTP_Request. - * - * @category HTTP - * @package HTTP_Request2 - * @author Alexey Borzov - * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License - * @version Release: 2.2.1 - * @link http://pear.php.net/package/HTTP_Request2 - * @link http://pear.php.net/bugs/bug.php?id=19332 - * @link http://tools.ietf.org/html/rfc1928 - */ -class HTTP_Request2_SocketWrapper -{ - /** - * PHP warning messages raised during stream_socket_client() call - * @var array - */ - protected $connectionWarnings = array(); - - /** - * Connected socket - * @var resource - */ - protected $socket; - - /** - * Sum of start time and global timeout, exception will be thrown if request continues past this time - * @var integer - */ - protected $deadline; - - /** - * Global timeout value, mostly for exception messages - * @var integer - */ - protected $timeout; - - /** - * Class constructor, tries to establish connection - * - * @param string $address Address for stream_socket_client() call, - * e.g. 'tcp://localhost:80' - * @param int $timeout Connection timeout (seconds) - * @param array $contextOptions Context options - * - * @throws HTTP_Request2_LogicException - * @throws HTTP_Request2_ConnectionException - */ - public function __construct($address, $timeout, array $contextOptions = array()) - { - if (!empty($contextOptions) - && !isset($contextOptions['socket']) && !isset($contextOptions['ssl']) - ) { - // Backwards compatibility with 2.1.0 and 2.1.1 releases - $contextOptions = array('ssl' => $contextOptions); - } - $context = stream_context_create(); - foreach ($contextOptions as $wrapper => $options) { - foreach ($options as $name => $value) { - if (!stream_context_set_option($context, $wrapper, $name, $value)) { - throw new HTTP_Request2_LogicException( - "Error setting '{$wrapper}' wrapper context option '{$name}'" - ); - } - } - } - set_error_handler(array($this, 'connectionWarningsHandler')); - $this->socket = stream_socket_client( - $address, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context - ); - restore_error_handler(); - // if we fail to bind to a specified local address (see request #19515), - // connection still succeeds, albeit with a warning. Throw an Exception - // with the warning text in this case as that connection is unlikely - // to be what user wants and as Curl throws an error in similar case. - if ($this->connectionWarnings) { - if ($this->socket) { - fclose($this->socket); - } - $error = $errstr ? $errstr : implode("\n", $this->connectionWarnings); - throw new HTTP_Request2_ConnectionException( - "Unable to connect to {$address}. Error: {$error}", 0, $errno - ); - } - } - - /** - * Destructor, disconnects socket - */ - public function __destruct() - { - fclose($this->socket); - } - - /** - * Wrapper around fread(), handles global request timeout - * - * @param int $length Reads up to this number of bytes - * - * @return string Data read from socket - * @throws HTTP_Request2_MessageException In case of timeout - */ - public function read($length) - { - if ($this->deadline) { - stream_set_timeout($this->socket, max($this->deadline - time(), 1)); - } - $data = fread($this->socket, $length); - $this->checkTimeout(); - return $data; - } - - /** - * Reads until either the end of the socket or a newline, whichever comes first - * - * Strips the trailing newline from the returned data, handles global - * request timeout. Method idea borrowed from Net_Socket PEAR package. - * - * @param int $bufferSize buffer size to use for reading - * @param int $localTimeout timeout value to use just for this call - * (used when waiting for "100 Continue" response) - * - * @return string Available data up to the newline (not including newline) - * @throws HTTP_Request2_MessageException In case of timeout - */ - public function readLine($bufferSize, $localTimeout = null) - { - $line = ''; - while (!feof($this->socket)) { - if (null !== $localTimeout) { - stream_set_timeout($this->socket, $localTimeout); - } elseif ($this->deadline) { - stream_set_timeout($this->socket, max($this->deadline - time(), 1)); - } - - $line .= @fgets($this->socket, $bufferSize); - - if (null === $localTimeout) { - $this->checkTimeout(); - - } else { - $info = stream_get_meta_data($this->socket); - // reset socket timeout if we don't have request timeout specified, - // prevents further calls failing with a bogus Exception - if (!$this->deadline) { - $default = (int)@ini_get('default_socket_timeout'); - stream_set_timeout($this->socket, $default > 0 ? $default : PHP_INT_MAX); - } - if ($info['timed_out']) { - throw new HTTP_Request2_MessageException( - "readLine() call timed out", HTTP_Request2_Exception::TIMEOUT - ); - } - } - if (substr($line, -1) == "\n") { - return rtrim($line, "\r\n"); - } - } - return $line; - } - - /** - * Wrapper around fwrite(), handles global request timeout - * - * @param string $data String to be written - * - * @return int - * @throws HTTP_Request2_MessageException - */ - public function write($data) - { - if ($this->deadline) { - stream_set_timeout($this->socket, max($this->deadline - time(), 1)); - } - $written = fwrite($this->socket, $data); - $this->checkTimeout(); - // http://www.php.net/manual/en/function.fwrite.php#96951 - if ($written < strlen($data)) { - throw new HTTP_Request2_MessageException('Error writing request'); - } - return $written; - } - - /** - * Tests for end-of-file on a socket - * - * @return bool - */ - public function eof() - { - return feof($this->socket); - } - - /** - * Sets request deadline - * - * @param int $deadline Exception will be thrown if request continues - * past this time - * @param int $timeout Original request timeout value, to use in - * Exception message - */ - public function setDeadline($deadline, $timeout) - { - $this->deadline = $deadline; - $this->timeout = $timeout; - } - - /** - * Turns on encryption on a socket - * - * @throws HTTP_Request2_ConnectionException - */ - public function enableCrypto() - { - $modes = array( - STREAM_CRYPTO_METHOD_TLS_CLIENT, - STREAM_CRYPTO_METHOD_SSLv3_CLIENT, - STREAM_CRYPTO_METHOD_SSLv23_CLIENT, - STREAM_CRYPTO_METHOD_SSLv2_CLIENT - ); - - foreach ($modes as $mode) { - if (stream_socket_enable_crypto($this->socket, true, $mode)) { - return; - } - } - throw new HTTP_Request2_ConnectionException( - 'Failed to enable secure connection when connecting through proxy' - ); - } - - /** - * Throws an Exception if stream timed out - * - * @throws HTTP_Request2_MessageException - */ - protected function checkTimeout() - { - $info = stream_get_meta_data($this->socket); - if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { - $reason = $this->deadline - ? "after {$this->timeout} second(s)" - : 'due to default_socket_timeout php.ini setting'; - throw new HTTP_Request2_MessageException( - "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT - ); - } - } - - /** - * Error handler to use during stream_socket_client() call - * - * One stream_socket_client() call may produce *multiple* PHP warnings - * (especially OpenSSL-related), we keep them in an array to later use for - * the message of HTTP_Request2_ConnectionException - * - * @param int $errno error level - * @param string $errstr error message - * - * @return bool - */ - protected function connectionWarningsHandler($errno, $errstr) - { - if ($errno & E_WARNING) { - array_unshift($this->connectionWarnings, $errstr); - } - return true; - } -} -?> + + * @copyright 2008-2016 Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Exception classes for HTTP_Request2 package */ +require_once 'HTTP/Request2/Exception.php'; + +/** + * Socket wrapper class used by Socket Adapter + * + * Needed to properly handle connection errors, global timeout support and + * similar things. Loosely based on Net_Socket used by older HTTP_Request. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License + * @version Release: 2.3.0 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://pear.php.net/bugs/bug.php?id=19332 + * @link http://tools.ietf.org/html/rfc1928 + */ +class HTTP_Request2_SocketWrapper +{ + /** + * PHP warning messages raised during stream_socket_client() call + * @var array + */ + protected $connectionWarnings = array(); + + /** + * Connected socket + * @var resource + */ + protected $socket; + + /** + * Sum of start time and global timeout, exception will be thrown if request continues past this time + * @var integer + */ + protected $deadline; + + /** + * Global timeout value, mostly for exception messages + * @var integer + */ + protected $timeout; + + /** + * Class constructor, tries to establish connection + * + * @param string $address Address for stream_socket_client() call, + * e.g. 'tcp://localhost:80' + * @param int $timeout Connection timeout (seconds) + * @param array $contextOptions Context options + * + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_ConnectionException + */ + public function __construct($address, $timeout, array $contextOptions = array()) + { + if (!empty($contextOptions) + && !isset($contextOptions['socket']) && !isset($contextOptions['ssl']) + ) { + // Backwards compatibility with 2.1.0 and 2.1.1 releases + $contextOptions = array('ssl' => $contextOptions); + } + if (isset($contextOptions['ssl'])) { + $contextOptions['ssl'] += array( + // Using "Intermediate compatibility" cipher bundle from + // https://wiki.mozilla.org/Security/Server_Side_TLS + 'ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:' + . 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:' + . 'DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:' + . 'ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:' + . 'ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:' + . 'ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:' + . 'ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:' + . 'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:' + . 'DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:' + . 'ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:' + . 'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:' + . 'AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:' + . '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA' + ); + if (version_compare(phpversion(), '5.4.13', '>=')) { + $contextOptions['ssl']['disable_compression'] = true; + if (version_compare(phpversion(), '5.6', '>=')) { + $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + } + } + $context = stream_context_create(); + foreach ($contextOptions as $wrapper => $options) { + foreach ($options as $name => $value) { + if (!stream_context_set_option($context, $wrapper, $name, $value)) { + throw new HTTP_Request2_LogicException( + "Error setting '{$wrapper}' wrapper context option '{$name}'" + ); + } + } + } + set_error_handler(array($this, 'connectionWarningsHandler')); + $this->socket = stream_socket_client( + $address, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context + ); + restore_error_handler(); + // if we fail to bind to a specified local address (see request #19515), + // connection still succeeds, albeit with a warning. Throw an Exception + // with the warning text in this case as that connection is unlikely + // to be what user wants and as Curl throws an error in similar case. + if ($this->connectionWarnings) { + if ($this->socket) { + fclose($this->socket); + } + $error = $errstr ? $errstr : implode("\n", $this->connectionWarnings); + throw new HTTP_Request2_ConnectionException( + "Unable to connect to {$address}. Error: {$error}", 0, $errno + ); + } + } + + /** + * Destructor, disconnects socket + */ + public function __destruct() + { + fclose($this->socket); + } + + /** + * Wrapper around fread(), handles global request timeout + * + * @param int $length Reads up to this number of bytes + * + * @return string Data read from socket + * @throws HTTP_Request2_MessageException In case of timeout + */ + public function read($length) + { + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + } + $data = fread($this->socket, $length); + $this->checkTimeout(); + return $data; + } + + /** + * Reads until either the end of the socket or a newline, whichever comes first + * + * Strips the trailing newline from the returned data, handles global + * request timeout. Method idea borrowed from Net_Socket PEAR package. + * + * @param int $bufferSize buffer size to use for reading + * @param int $localTimeout timeout value to use just for this call + * (used when waiting for "100 Continue" response) + * + * @return string Available data up to the newline (not including newline) + * @throws HTTP_Request2_MessageException In case of timeout + */ + public function readLine($bufferSize, $localTimeout = null) + { + $line = ''; + while (!feof($this->socket)) { + if (null !== $localTimeout) { + stream_set_timeout($this->socket, $localTimeout); + } elseif ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + } + + $line .= @fgets($this->socket, $bufferSize); + + if (null === $localTimeout) { + $this->checkTimeout(); + + } else { + $info = stream_get_meta_data($this->socket); + // reset socket timeout if we don't have request timeout specified, + // prevents further calls failing with a bogus Exception + if (!$this->deadline) { + $default = (int)@ini_get('default_socket_timeout'); + stream_set_timeout($this->socket, $default > 0 ? $default : PHP_INT_MAX); + } + if ($info['timed_out']) { + throw new HTTP_Request2_MessageException( + "readLine() call timed out", HTTP_Request2_Exception::TIMEOUT + ); + } + } + if (substr($line, -1) == "\n") { + return rtrim($line, "\r\n"); + } + } + return $line; + } + + /** + * Wrapper around fwrite(), handles global request timeout + * + * @param string $data String to be written + * + * @return int + * @throws HTTP_Request2_MessageException + */ + public function write($data) + { + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + } + $written = fwrite($this->socket, $data); + $this->checkTimeout(); + // http://www.php.net/manual/en/function.fwrite.php#96951 + if ($written < strlen($data)) { + throw new HTTP_Request2_MessageException('Error writing request'); + } + return $written; + } + + /** + * Tests for end-of-file on a socket + * + * @return bool + */ + public function eof() + { + return feof($this->socket); + } + + /** + * Sets request deadline + * + * @param int $deadline Exception will be thrown if request continues + * past this time + * @param int $timeout Original request timeout value, to use in + * Exception message + */ + public function setDeadline($deadline, $timeout) + { + $this->deadline = $deadline; + $this->timeout = $timeout; + } + + /** + * Turns on encryption on a socket + * + * @throws HTTP_Request2_ConnectionException + */ + public function enableCrypto() + { + if (version_compare(phpversion(), '5.6', '<')) { + $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT; + } else { + $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + } + + if (!stream_socket_enable_crypto($this->socket, true, $cryptoMethod)) { + throw new HTTP_Request2_ConnectionException( + 'Failed to enable secure connection when connecting through proxy' + ); + } + } + + /** + * Throws an Exception if stream timed out + * + * @throws HTTP_Request2_MessageException + */ + protected function checkTimeout() + { + $info = stream_get_meta_data($this->socket); + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + $reason = $this->deadline + ? "after {$this->timeout} second(s)" + : 'due to default_socket_timeout php.ini setting'; + throw new HTTP_Request2_MessageException( + "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT + ); + } + } + + /** + * Error handler to use during stream_socket_client() call + * + * One stream_socket_client() call may produce *multiple* PHP warnings + * (especially OpenSSL-related), we keep them in an array to later use for + * the message of HTTP_Request2_ConnectionException + * + * @param int $errno error level + * @param string $errstr error message + * + * @return bool + */ + protected function connectionWarningsHandler($errno, $errstr) + { + if ($errno & E_WARNING) { + array_unshift($this->connectionWarnings, $errstr); + } + return true; + } +} +?>