From d2c886023c7f820b88dcab75e95ab0905ea2b27f Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 22 Jun 2011 15:56:27 -0400 Subject: [PATCH] update HTTP_Request2 to 2.0.0RC1 --- extlib/HTTP/Request2.php | 351 ++++++++++++++++++------ extlib/HTTP/Request2/Adapter.php | 16 +- extlib/HTTP/Request2/Adapter/Curl.php | 235 ++++++++++++++-- extlib/HTTP/Request2/Adapter/Mock.php | 22 +- extlib/HTTP/Request2/Adapter/Socket.php | 279 +++++++++++++------ extlib/HTTP/Request2/Exception.php | 114 +++++++- extlib/HTTP/Request2/MultipartBody.php | 8 +- extlib/HTTP/Request2/Observer/Log.php | 24 +- extlib/HTTP/Request2/Response.php | 154 ++++++++--- 9 files changed, 922 insertions(+), 281 deletions(-) diff --git a/extlib/HTTP/Request2.php b/extlib/HTTP/Request2.php index e06bb86bca..60beeaf2ef 100644 --- a/extlib/HTTP/Request2.php +++ b/extlib/HTTP/Request2.php @@ -1,12 +1,12 @@ + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Request2.php 278226 2009-04-03 21:32:48Z avb $ + * @version SVN: $Id: Request2.php 308735 2011-02-27 20:31:28Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -48,16 +48,16 @@ require_once 'Net/URL2.php'; /** * Exception class for HTTP_Request2 package - */ + */ require_once 'HTTP/Request2/Exception.php'; /** - * Class representing a HTTP request + * Class representing a HTTP request message * * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 * @link http://tools.ietf.org/html/rfc2616#section-5 */ class HTTP_Request2 implements SplSubject @@ -78,7 +78,7 @@ class HTTP_Request2 implements SplSubject /**#@-*/ /**#@+ - * Constants for HTTP authentication schemes + * Constants for HTTP authentication schemes * * @link http://tools.ietf.org/html/rfc2617 */ @@ -95,7 +95,7 @@ class HTTP_Request2 implements SplSubject /** * Regular expression used to check for invalid symbols in cookie strings * @link http://pear.php.net/bugs/bug.php?id=15630 - * @link http://cgi.netscape.com/newsref/std/cookie_spec.html + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html */ const REGEXP_INVALID_COOKIE = '/[\s,;]/'; @@ -164,7 +164,11 @@ class HTTP_Request2 implements SplSubject 'ssl_local_cert' => null, 'ssl_passphrase' => null, - 'digest_compat_ie' => false + 'digest_compat_ie' => false, + + 'follow_redirects' => false, + 'max_redirects' => 5, + 'strict_redirects' => false ); /** @@ -191,7 +195,7 @@ class HTTP_Request2 implements SplSubject protected $postParams = array(); /** - * Array of file uploads (for multipart/form-data POST requests) + * Array of file uploads (for multipart/form-data POST requests) * @var array */ protected $uploads = array(); @@ -202,11 +206,16 @@ class HTTP_Request2 implements SplSubject */ 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. + * Also sets a default value for User-Agent header. * * @param string|Net_Url2 Request URL * @param string Request method @@ -214,14 +223,14 @@ class HTTP_Request2 implements SplSubject */ 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->setConfig($config); - $this->setHeader('user-agent', 'HTTP_Request2/0.4.1 ' . + $this->setHeader('user-agent', 'HTTP_Request2/2.0.0RC1 ' . '(http://pear.php.net/package/http_request2) ' . 'PHP/' . phpversion()); } @@ -235,15 +244,20 @@ class HTTP_Request2 implements SplSubject * * @param string|Net_URL2 Request URL * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ public function setUrl($url) { if (is_string($url)) { - $url = new Net_URL2($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_Exception('Parameter is not a valid HTTP URL'); + throw new HTTP_Request2_LogicException( + 'Parameter is not a valid HTTP URL', + HTTP_Request2_Exception::INVALID_ARGUMENT + ); } // URL contains username / password? if ($url->getUserinfo()) { @@ -275,13 +289,16 @@ class HTTP_Request2 implements SplSubject * * @param string * @return HTTP_Request2 - * @throws HTTP_Request2_Exception if the method name is invalid + * @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_Exception("Invalid request method '{$method}'"); + throw new HTTP_Request2_LogicException( + "Invalid request method '{$method}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); } $this->method = $method; @@ -306,7 +323,7 @@ class HTTP_Request2 implements SplSubject *
  • '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 + * 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)
  • @@ -324,7 +341,7 @@ class HTTP_Request2 implements SplSubject * 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 + *
  • '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 @@ -332,13 +349,19 @@ class HTTP_Request2 implements SplSubject *
  • '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 configuration parameter name or array * ('parameter name' => 'parameter value') * @param mixed parameter value if $nameOrConfig is not an array * @return HTTP_Request2 - * @throws HTTP_Request2_Exception If the parameter is unknown + * @throws HTTP_Request2_LogicException If the parameter is unknown */ public function setConfig($nameOrConfig, $value = null) { @@ -349,8 +372,9 @@ class HTTP_Request2 implements SplSubject } else { if (!array_key_exists($nameOrConfig, $this->config)) { - throw new HTTP_Request2_Exception( - "Unknown configuration parameter '{$nameOrConfig}'" + throw new HTTP_Request2_LogicException( + "Unknown configuration parameter '{$nameOrConfig}'", + HTTP_Request2_Exception::INVALID_ARGUMENT ); } $this->config[$nameOrConfig] = $value; @@ -363,17 +387,18 @@ class HTTP_Request2 implements SplSubject * Returns the value(s) of the configuration parameter(s) * * @param string parameter name - * @return mixed value of $name parameter, array of all configuration + * @return mixed value of $name parameter, array of all configuration * parameters if $name is not given - * @throws HTTP_Request2_Exception If the parameter is unknown + * @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_Exception( - "Unknown configuration parameter '{$name}'" + throw new HTTP_Request2_LogicException( + "Unknown configuration parameter '{$name}'", + HTTP_Request2_Exception::INVALID_ARGUMENT ); } return $this->config[$name]; @@ -386,7 +411,7 @@ class HTTP_Request2 implements SplSubject * @param string password * @param string authentication scheme * @return HTTP_Request2 - */ + */ public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC) { if (empty($user)) { @@ -419,13 +444,13 @@ class HTTP_Request2 implements SplSubject * 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 + * 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' @@ -435,18 +460,21 @@ class HTTP_Request2 implements SplSubject * * @param string|array header name, header string ('Header: value') * or an array of headers - * @param string|null header value, header will be removed if null + * @param string|array|null header value if $name is not an array, + * header will be removed if value is null + * @param bool whether to replace previous header with the + * same name or append to its value * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ - public function setHeader($name, $value = null) + 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); + $this->setHeader($k, $v, $replace); } else { - $this->setHeader($v); + $this->setHeader($v, null, $replace); } } } else { @@ -455,17 +483,30 @@ class HTTP_Request2 implements SplSubject } // 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_Exception("Invalid header name '{$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 { - $this->headers[$name] = $value; + 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; } @@ -483,21 +524,39 @@ class HTTP_Request2 implements SplSubject } /** - * Appends a cookie to "Cookie:" header + * 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 cookie name * @param string cookie value * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException + * @see setCookieJar() */ public function addCookie($name, $value) { - $cookie = $name . '=' . $value; - if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) { - throw new HTTP_Request2_Exception("Invalid cookie: '{$cookie}'"); + 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); } - $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; '; - $this->setHeader('cookie', $cookies . $cookie); return $this; } @@ -505,24 +564,32 @@ class HTTP_Request2 implements SplSubject /** * Sets the request body * - * @param string Either a string with the body or filename containing body + * If you provide file pointer rather than file name, it should support + * fstat() and rewind() operations. + * + * @param string|resource|HTTP_Request2_MultipartBody Either a string + * with the body or filename containing body or pointer to + * an open file or object with multipart body data * @param bool Whether first parameter is a filename * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ public function setBody($body, $isFilename = false) { - if (!$isFilename) { - $this->body = (string)$body; - } else { - if (!($fp = @fopen($body, 'rb'))) { - throw new HTTP_Request2_Exception("Cannot open file {$body}"); + if (!$isFilename && !is_resource($body)) { + if (!$body instanceof HTTP_Request2_MultipartBody) { + $this->body = (string)$body; + } else { + $this->body = $body; } - $this->body = $fp; + } else { + $fileData = $this->fopenWrapper($body, empty($this->headers['content-type'])); + $this->body = $fileData['fp']; if (empty($this->headers['content-type'])) { - $this->setHeader('content-type', self::detectMimeType($body)); + $this->setHeader('content-type', $fileData['type']); } } + $this->postParams = $this->uploads = array(); return $this; } @@ -534,10 +601,10 @@ class HTTP_Request2 implements SplSubject */ public function getBody() { - if (self::METHOD_POST == $this->method && + if (self::METHOD_POST == $this->method && (!empty($this->postParams) || !empty($this->uploads)) ) { - if ('application/x-www-form-urlencoded' == $this->headers['content-type']) { + 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); @@ -545,7 +612,7 @@ class HTTP_Request2 implements SplSubject // support RFC 3986 by not encoding '~' symbol (request #15368) return str_replace('%7E', '~', $body); - } elseif ('multipart/form-data' == $this->headers['content-type']) { + } 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') @@ -564,25 +631,28 @@ class HTTP_Request2 implements SplSubject * 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 name of file-upload field - * @param mixed full name of local file - * @param string filename to send in the request + * @param string|resource|array full name of local file, pointer to + * open file or an array of files + * @param string filename to send in the request * @param string content-type of file being uploaded * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ public function addUpload($fieldName, $filename, $sendFilename = null, $contentType = null) { if (!is_array($filename)) { - if (!($fp = @fopen($filename, 'rb'))) { - throw new HTTP_Request2_Exception("Cannot open file {$filename}"); - } + $fileData = $this->fopenWrapper($filename, empty($contentType)); $this->uploads[$fieldName] = array( - 'fp' => $fp, - 'filename' => empty($sendFilename)? basename($filename): $sendFilename, - 'size' => filesize($filename), - 'type' => empty($contentType)? self::detectMimeType($filename): $contentType + '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(); @@ -590,13 +660,12 @@ class HTTP_Request2 implements SplSubject if (!is_array($f)) { $f = array($f); } - if (!($fp = @fopen($f[0], 'rb'))) { - throw new HTTP_Request2_Exception("Cannot open file {$f[0]}"); - } - $fps[] = $fp; - $names[] = empty($f[1])? basename($f[0]): $f[1]; - $sizes[] = filesize($f[0]); - $types[] = empty($f[2])? self::detectMimeType($f[0]): $f[2]; + $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 @@ -703,8 +772,10 @@ class HTTP_Request2 implements SplSubject *
  • '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, + *
  • '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 @@ -738,7 +809,7 @@ class HTTP_Request2 implements SplSubject * * @param string|HTTP_Request2_Adapter * @return HTTP_Request2 - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ public function setAdapter($adapter) { @@ -751,19 +822,67 @@ class HTTP_Request2 implements SplSubject include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php'; } if (!class_exists($adapter, false)) { - throw new HTTP_Request2_Exception("Class {$adapter} not found"); + 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_Exception('Parameter is not a HTTP request 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 Existing CookieJar object, true to + * create a new one, false to remove + */ + 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 * @@ -773,20 +892,25 @@ class HTTP_Request2 implements SplSubject public function send() { // Sanity check for URL - if (!$this->url instanceof Net_URL2) { - throw new HTTP_Request2_Exception('No URL given'); - } elseif (!$this->url->isAbsolute()) { - throw new HTTP_Request2_Exception('Absolute URL required'); - } elseif (!in_array(strtolower($this->url->getScheme()), array('https', 'http'))) { - throw new HTTP_Request2_Exception('Not a HTTP 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 + ? 'none' : "'" . $this->url->__toString() . "'") + . ' 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 - if ($magicQuotes = ini_get('magic_quotes_runtime')) { - ini_set('magic_quotes_runtime', false); + // 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 @@ -801,7 +925,7 @@ class HTTP_Request2 implements SplSubject } // cleanup in either case (poor man's "finally" clause) if ($magicQuotes) { - ini_set('magic_quotes_runtime', true); + set_magic_quotes_runtime(true); } if (!empty($oldEncoding)) { mb_internal_encoding($oldEncoding); @@ -813,6 +937,53 @@ class HTTP_Request2 implements SplSubject return $response; } + /** + * Wrapper around fopen()/fstat() used by setBody() and addUpload() + * + * @param string|resource file name or pointer to open file + * @param bool 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)) { + $track = @ini_set('track_errors', 1); + if (!($fileData['fp'] = @fopen($file, 'rb'))) { + $e = new HTTP_Request2_LogicException( + $php_errormsg, HTTP_Request2_Exception::READ_ERROR + ); + } + @ini_set('track_errors', $track); + if (isset($e)) { + throw $e; + } + 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 * @@ -825,12 +996,12 @@ class HTTP_Request2 implements SplSubject */ protected static function detectMimeType($filename) { - // finfo extension from PECL available + // finfo extension from PECL available if (function_exists('finfo_open')) { if (!isset(self::$_fileinfoDb)) { self::$_fileinfoDb = @finfo_open(FILEINFO_MIME); } - if (self::$_fileinfoDb) { + if (self::$_fileinfoDb) { $info = finfo_file(self::$_fileinfoDb, $filename); } } diff --git a/extlib/HTTP/Request2/Adapter.php b/extlib/HTTP/Request2/Adapter.php index 39b092b346..2cabbf897b 100644 --- a/extlib/HTTP/Request2/Adapter.php +++ b/extlib/HTTP/Request2/Adapter.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Adapter.php 274684 2009-01-26 23:07:27Z avb $ + * @version SVN: $Id: Adapter.php 308322 2011-02-14 13:58:03Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -50,13 +50,13 @@ 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 + * 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 - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 */ abstract class HTTP_Request2_Adapter { @@ -109,8 +109,8 @@ abstract class HTTP_Request2_Adapter /** * Calculates length of the request body, adds proper headers * - * @param array associative array of request headers, this method will - * add proper 'Content-Length' and 'Content-Type' headers + * @param array 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) @@ -133,13 +133,15 @@ abstract class HTTP_Request2_Adapter if (in_array($this->request->getMethod(), self::$bodyDisallowed) || 0 == $this->contentLength ) { - unset($headers['content-type']); // 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'])) { diff --git a/extlib/HTTP/Request2/Adapter/Curl.php b/extlib/HTTP/Request2/Adapter/Curl.php index 4d4de0dcc7..fecfbd7abc 100644 --- a/extlib/HTTP/Request2/Adapter/Curl.php +++ b/extlib/HTTP/Request2/Adapter/Curl.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Curl.php 278226 2009-04-03 21:32:48Z avb $ + * @version SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -52,7 +52,7 @@ require_once 'HTTP/Request2/Adapter.php'; * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 */ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter { @@ -79,6 +79,46 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter '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 @@ -110,6 +150,26 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter */ protected $lastInfo; + /** + * Creates a subclass of HTTP_Request2_Exception from curl error data + * + * @param resource 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 * @@ -120,7 +180,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter public function sendRequest(HTTP_Request2 $request) { if (!extension_loaded('curl')) { - throw new HTTP_Request2_Exception('cURL extension not available'); + throw new HTTP_Request2_LogicException( + 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION + ); } $this->request = $request; @@ -131,24 +193,30 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter try { if (false === curl_exec($ch = $this->createCurlHandle())) { - $errorMessage = 'Error sending request: #' . curl_errno($ch) . - ' ' . curl_error($ch); + $e = self::wrapCurlError($ch); } } catch (Exception $e) { } - $this->lastInfo = curl_getinfo($ch); - curl_close($ch); + 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; - } elseif (!empty($errorMessage)) { - throw new HTTP_Request2_Exception($errorMessage); + } + + if ($jar = $request->getCookieJar()) { + $jar->addCookiesFromResponse($response, $request->getUrl()); } if (0 < $this->lastInfo['size_download']) { - $this->request->setLastEvent('receivedBody', $this->response); + $request->setLastEvent('receivedBody', $response); } - return $this->response; + return $response; } /** @@ -165,19 +233,16 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter * 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_Exception + * @throws HTTP_Request2_LogicException */ protected function createCurlHandle() { $ch = curl_init(); curl_setopt_array($ch, array( - // setup callbacks - CURLOPT_READFUNCTION => array($this, 'callbackReadBody'), + // setup write callbacks CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'), CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'), - // disallow redirects - CURLOPT_FOLLOWLOCATION => false, // buffer size CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'), // connection timeout @@ -188,6 +253,27 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter 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); + } + } + // request timeout if ($timeout = $this->request->getConfig('timeout')) { curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); @@ -210,6 +296,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter 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()); } @@ -217,7 +309,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter // set proxy, if needed if ($host = $this->request->getConfig('proxy_host')) { if (!($port = $this->request->getConfig('proxy_port'))) { - throw new HTTP_Request2_Exception('Proxy port not provided'); + 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')) { @@ -246,13 +340,11 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } // set SSL options - if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) { - 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); - } + 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); } } @@ -262,6 +354,12 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter $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])) { @@ -271,6 +369,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } $this->calculateRequestLength($headers); + if (isset($headers['content-length'])) { + $this->workaroundPhpBug47204($ch, $headers); + } // set headers not having special keys $headersFmt = array(); @@ -283,13 +384,50 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter 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 cURL handle + * @param array 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')); + + // rewind may be needed, read the whole body into memory + } else { + 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 cURL handle * @param resource file descriptor (not used) * @param integer maximum length of data to return - * @return string part of the request body, up to $length bytes + * @return string part of the request body, up to $length bytes */ protected function callbackReadBody($ch, $fd, $length) { @@ -336,6 +474,19 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter '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) { @@ -344,7 +495,9 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter } } if (empty($this->response)) { - $this->response = new HTTP_Request2_Response($string, false); + $this->response = new HTTP_Request2_Response( + $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) + ); } else { $this->response->parseHeaderLine($string); if ('' == trim($string)) { @@ -352,6 +505,27 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter 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; } } @@ -368,10 +542,13 @@ class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter */ protected function callbackWriteBody($ch, $string) { - // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if + // 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_Exception("Malformed response: {$string}"); + throw new HTTP_Request2_MessageException( + "Malformed response: {$string}", + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); } if ($this->request->getConfig('store_body')) { $this->response->appendBody($string); diff --git a/extlib/HTTP/Request2/Adapter/Mock.php b/extlib/HTTP/Request2/Adapter/Mock.php index 89688003b2..c99defb899 100644 --- a/extlib/HTTP/Request2/Adapter/Mock.php +++ b/extlib/HTTP/Request2/Adapter/Mock.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Mock.php 274406 2009-01-23 18:01:57Z avb $ + * @version SVN: $Id: Mock.php 308322 2011-02-14 13:58:03Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -55,31 +55,31 @@ require_once 'HTTP/Request2/Adapter.php'; * * $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 - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 */ class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter { /** * A queue of responses to be returned by sendRequest() - * @var array + * @var array */ protected $responses = array(); /** * Returns the next response from the queue built by addResponse() * - * If the queue is empty will return default empty response with status 400, + * If the queue is empty it will return default empty response with status 400, * if an Exception object was added to the queue it will be thrown. * * @param HTTP_Request2 @@ -93,7 +93,7 @@ class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter if ($response instanceof HTTP_Request2_Response) { return $response; } else { - // rethrow the exception, + // rethrow the exception $class = get_class($response); $message = $response->getMessage(); $code = $response->getCode(); @@ -108,7 +108,7 @@ class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter * Adds response to the queue * * @param mixed either a string, a pointer to an open file, - * a HTTP_Request2_Response or Exception object + * an instance of HTTP_Request2_Response or Exception * @throws HTTP_Request2_Exception */ public function addResponse($response) @@ -135,7 +135,7 @@ class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter public static function createResponseFromString($str) { $parts = preg_split('!(\r?\n){2}!m', $str, 2); - $headerLines = explode("\n", $parts[0]); + $headerLines = explode("\n", $parts[0]); $response = new HTTP_Request2_Response(array_shift($headerLines)); foreach ($headerLines as $headerLine) { $response->parseHeaderLine($headerLine); diff --git a/extlib/HTTP/Request2/Adapter/Socket.php b/extlib/HTTP/Request2/Adapter/Socket.php index ff44d49594..05cc4c715b 100644 --- a/extlib/HTTP/Request2/Adapter/Socket.php +++ b/extlib/HTTP/Request2/Adapter/Socket.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Socket.php 279760 2009-05-03 10:46:42Z avb $ + * @version SVN: $Id: Socket.php 309921 2011-04-03 16:43:02Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -55,13 +55,13 @@ require_once 'HTTP/Request2/Adapter.php'; * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 */ 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]+'; /** @@ -79,11 +79,11 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter /** * Data for digest authentication scheme * - * The keys for the array are URL prefixes. + * 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 + * 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 @@ -110,18 +110,28 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter protected $proxyChallenge; /** - * Global timeout, exception will be raised if request continues past this time + * Sum of start time and global timeout, exception will be thrown if request continues past this time * @var integer */ - protected $timeout = null; + protected $deadline = null; /** * 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; + /** * Sends request to the remote server and returns its response * @@ -132,33 +142,38 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter public function sendRequest(HTTP_Request2 $request) { $this->request = $request; - $keepAlive = $this->connect(); - $headers = $this->prepareHeaders(); - // Use global request timeout if given, see feature requests #5735, #8964 + // Use global request timeout if given, see feature requests #5735, #8964 if ($timeout = $request->getConfig('timeout')) { - $this->timeout = time() + $timeout; + $this->deadline = time() + $timeout; } else { - $this->timeout = null; + $this->deadline = null; } try { + $keepAlive = $this->connect(); + $headers = $this->prepareHeaders(); if (false === @fwrite($this->socket, $headers, strlen($headers))) { - throw new HTTP_Request2_Exception('Error writing request'); + throw new HTTP_Request2_MessageException('Error writing request'); } // provide request headers to the observer, see request #7633 $this->request->setLastEvent('sentHeaders', $headers); $this->writeBody(); - if ($this->timeout && time() > $this->timeout) { - throw new HTTP_Request2_Exception( - 'Request timed out after ' . - $request->getConfig('timeout') . ' second(s)' + if ($this->deadline && time() > $this->deadline) { + throw new HTTP_Request2_MessageException( + 'Request timed out after ' . + $request->getConfig('timeout') . ' second(s)', + HTTP_Request2_Exception::TIMEOUT ); } $response = $this->readResponse(); + if ($jar = $request->getCookieJar()) { + $jar->addCookiesFromResponse($response, $request->getUrl()); + } + if (!$this->canKeepAlive($keepAlive, $response)) { $this->disconnect(); } @@ -178,10 +193,21 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } catch (Exception $e) { $this->disconnect(); + } + + unset($this->request, $this->requestBody); + + if (!empty($e)) { + $this->redirectCountdown = null; throw $e; } - return $response; + if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) { + $this->redirectCountdown = null; + return $response; + } else { + return $this->handleRedirect($request, $response); + } } /** @@ -202,7 +228,10 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter if ($host = $this->request->getConfig('proxy_host')) { if (!($port = $this->request->getConfig('proxy_port'))) { - throw new HTTP_Request2_Exception('Proxy port not provided'); + throw new HTTP_Request2_LogicException( + 'Proxy port not provided', + HTTP_Request2_Exception::MISSING_VALUE + ); } $proxy = true; } else { @@ -212,25 +241,27 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } if ($tunnel && !$proxy) { - throw new HTTP_Request2_Exception( - "Trying to perform CONNECT request without proxy" + 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_Exception( - 'Need OpenSSL support for https:// requests' + 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 ($proxy && !$secure && + if ($proxy && !$secure && !empty($headers['connection']) && 'Keep-Alive' == $headers['connection'] ) { $this->request->setHeader('connection'); } - $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && + $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && empty($headers['connection'])) || (!empty($headers['connection']) && 'Keep-Alive' == $headers['connection']); @@ -278,21 +309,27 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $context = stream_context_create(); foreach ($options as $name => $value) { if (!stream_context_set_option($context, 'ssl', $name, $value)) { - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_LogicException( "Error setting SSL context option '{$name}'" ); } } + $track = @ini_set('track_errors', 1); $this->socket = @stream_socket_client( $remote, $errno, $errstr, $this->request->getConfig('connect_timeout'), STREAM_CLIENT_CONNECT, $context ); if (!$this->socket) { - throw new HTTP_Request2_Exception( - "Unable to connect to {$remote}. Error #{$errno}: {$errstr}" + $e = new HTTP_Request2_ConnectionException( + "Unable to connect to {$remote}. Error: " + . (empty($errstr)? $php_errormsg: $errstr), 0, $errno ); } + @ini_set('track_errors', $track); + if (isset($e)) { + throw $e; + } $this->request->setLastEvent('connect', $remote); self::$sockets[$socketKey] =& $this->socket; } @@ -320,7 +357,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $response = $connect->send(); // Need any successful (2XX) response if (200 > $response->getStatus() || 300 <= $response->getStatus()) { - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_ConnectionException( 'Failed to connect via HTTPS proxy. Proxy response: ' . $response->getStatus() . ' ' . $response->getReasonPhrase() ); @@ -328,10 +365,10 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $this->socket = $donor->socket; $modes = array( - STREAM_CRYPTO_METHOD_TLS_CLIENT, + STREAM_CRYPTO_METHOD_TLS_CLIENT, STREAM_CRYPTO_METHOD_SSLv3_CLIENT, STREAM_CRYPTO_METHOD_SSLv23_CLIENT, - STREAM_CRYPTO_METHOD_SSLv2_CLIENT + STREAM_CRYPTO_METHOD_SSLv2_CLIENT ); foreach ($modes as $mode) { @@ -339,7 +376,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter return; } } - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_ConnectionException( 'Failed to enable secure connection when connecting through proxy' ); } @@ -347,7 +384,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter /** * Checks whether current connection may be reused or should be closed * - * @param boolean whether connection could be persistent + * @param boolean whether connection could be persistent * in the first place * @param HTTP_Request2_Response response object to check * @return boolean @@ -361,8 +398,11 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter return true; } - $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) || - null !== $response->getHeader('content-length'); + $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()); @@ -381,6 +421,66 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } } + /** + * 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 Original request + * @param HTTP_Request2_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 * @@ -389,7 +489,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * - 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 + * 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 @@ -453,7 +553,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * - 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 + * 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 @@ -489,7 +589,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value * * There is a problem with implementation of RFC 2617: several of the parameters - * here are defined as quoted-string and thus may contain backslash escaped + * 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. @@ -501,17 +601,17 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * - 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 + * - 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 + * quoted-string handling, unfortunately that means failure to authorize * sometimes * * @param string 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_Exception in case of unsupported challenge parameters + * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters */ protected function parseDigestChallenge($headerValue) { @@ -537,23 +637,23 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } } // we only support qop=auth - if (!empty($paramsAry['qop']) && + if (!empty($paramsAry['qop']) && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))) ) { - throw new HTTP_Request2_Exception( + 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_Exception( + throw new HTTP_Request2_NotImplementedException( "Only 'MD5' algorithm is currently supported in digest authentication, " . "server requested '{$paramsAry['algorithm']}'" ); } - return $paramsAry; + return $paramsAry; } /** @@ -562,7 +662,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * @param array challenge to update * @param string value of [Proxy-]Authentication-Info header * @todo validate server rspauth response - */ + */ protected function updateChallenge(&$challenge, $headerValue) { $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . @@ -593,10 +693,10 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * @param array 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, '?')) && + if (false !== ($q = strpos($url, '?')) && $this->request->getConfig('digest_compat_ie') ) { $url = substr($url, 0, $q); @@ -621,7 +721,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter 'nonce="' . $challenge['nonce'] . '", ' . 'uri="' . $url . '", ' . 'response="' . $digest . '"' . - (!empty($challenge['opaque'])? + (!empty($challenge['opaque'])? ', opaque="' . $challenge['opaque'] . '"': '') . (!empty($challenge['qop'])? @@ -635,7 +735,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * @param array request headers * @param string request host (needed for digest authentication) * @param string request URL (needed for digest authentication) - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_NotImplementedException */ protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl) { @@ -644,7 +744,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } switch ($auth['scheme']) { case HTTP_Request2::AUTH_BASIC: - $headers['authorization'] = + $headers['authorization'] = 'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']); break; @@ -657,7 +757,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter foreach (array_keys(self::$challenges) as $key) { if ($key == substr($fullUrl, 0, strlen($key))) { $headers['authorization'] = $this->createDigestResponse( - $auth['user'], $auth['password'], + $auth['user'], $auth['password'], $requestUrl, self::$challenges[$key] ); $this->serverChallenge =& self::$challenges[$key]; @@ -667,7 +767,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter break; default: - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_NotImplementedException( "Unknown HTTP authentication scheme '{$auth['scheme']}'" ); } @@ -678,7 +778,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * * @param array request headers * @param string request URL (needed for digest authentication) - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_NotImplementedException */ protected function addProxyAuthorizationHeader(&$headers, $requestUrl) { @@ -711,7 +811,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter break; default: - throw new HTTP_Request2_Exception( + throw new HTTP_Request2_NotImplementedException( "Unknown HTTP authentication scheme '" . $this->request->getConfig('proxy_auth_scheme') . "'" ); @@ -762,6 +862,11 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter ) { $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); @@ -779,7 +884,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter /** * Sends the request body * - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_MessageException */ protected function writeBody() { @@ -800,12 +905,13 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $str = $this->requestBody->read($bufferSize); } if (false === @fwrite($this->socket, $str, strlen($str))) { - throw new HTTP_Request2_Exception('Error writing request'); + throw new HTTP_Request2_MessageException('Error writing request'); } // Provide the length of written string to the observer, request #7630 $this->request->setLastEvent('sentBodyPart', strlen($str)); - $position += strlen($str); + $position += strlen($str); } + $this->request->setLastEvent('sentBody', $this->contentLength); } /** @@ -819,7 +925,9 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter $bufferSize = $this->request->getConfig('buffer_size'); do { - $response = new HTTP_Request2_Response($this->readLine($bufferSize), true); + $response = new HTTP_Request2_Response( + $this->readLine($bufferSize), true, $this->request->getUrl() + ); do { $headerLine = $this->readLine($bufferSize); $response->parseHeaderLine($headerLine); @@ -880,28 +988,30 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter } /** - * Reads until either the end of the socket or a newline, whichever comes first + * 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. + * Strips the trailing newline from the returned data, handles global + * request timeout. Method idea borrowed from Net_Socket PEAR package. * * @param int buffer size to use for reading * @return Available data up to the newline (not including newline) - * @throws HTTP_Request2_Exception In case of timeout + * @throws HTTP_Request2_MessageException In case of timeout */ protected function readLine($bufferSize) { $line = ''; while (!feof($this->socket)) { - if ($this->timeout) { - stream_set_timeout($this->socket, max($this->timeout - time(), 1)); + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); } $line .= @fgets($this->socket, $bufferSize); $info = stream_get_meta_data($this->socket); - if ($info['timed_out'] || $this->timeout && time() > $this->timeout) { - throw new HTTP_Request2_Exception( - 'Request timed out after ' . - $this->request->getConfig('timeout') . ' second(s)' + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + $reason = $this->deadline + ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' + : 'due to default_socket_timeout php.ini setting'; + throw new HTTP_Request2_MessageException( + "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT ); } if (substr($line, -1) == "\n") { @@ -916,19 +1026,21 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * * @param int Reads up to this number of bytes * @return Data read from socket - * @throws HTTP_Request2_Exception In case of timeout + * @throws HTTP_Request2_MessageException In case of timeout */ protected function fread($length) { - if ($this->timeout) { - stream_set_timeout($this->socket, max($this->timeout - time(), 1)); + if ($this->deadline) { + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); } $data = fread($this->socket, $length); $info = stream_get_meta_data($this->socket); - if ($info['timed_out'] || $this->timeout && time() > $this->timeout) { - throw new HTTP_Request2_Exception( - 'Request timed out after ' . - $this->request->getConfig('timeout') . ' second(s)' + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + $reason = $this->deadline + ? 'after ' . $this->request->getConfig('timeout') . ' second(s)' + : 'due to default_socket_timeout php.ini setting'; + throw new HTTP_Request2_MessageException( + "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT ); } return $data; @@ -939,7 +1051,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter * * @param int buffer size to use for reading * @return string - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_MessageException */ protected function readChunked($bufferSize) { @@ -947,8 +1059,9 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter if (0 == $this->chunkLength) { $line = $this->readLine($bufferSize); if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { - throw new HTTP_Request2_Exception( - "Cannot decode chunked response, invalid chunk length '{$line}'" + throw new HTTP_Request2_MessageException( + "Cannot decode chunked response, invalid chunk length '{$line}'", + HTTP_Request2_Exception::DECODE_ERROR ); } else { $this->chunkLength = hexdec($matches[1]); diff --git a/extlib/HTTP/Request2/Exception.php b/extlib/HTTP/Request2/Exception.php index bfef7d6c22..530c23b9ce 100644 --- a/extlib/HTTP/Request2/Exception.php +++ b/extlib/HTTP/Request2/Exception.php @@ -1,12 +1,12 @@ + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Exception.php 273003 2009-01-07 19:28:22Z avb $ + * @version SVN: $Id: Exception.php 308629 2011-02-24 17:34:24Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -47,16 +47,114 @@ require_once 'PEAR/Exception.php'; /** - * Exception class for HTTP_Request2 package - * - * Such a class is required by the Exception RFC: - * http://pear.php.net/pepr/pepr-proposal-show.php?id=132 + * Base exception class for HTTP_Request2 package * * @category HTTP * @package HTTP_Request2 - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 + * @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 exception message + * @param int package error code, one of class constants + * @param int 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 + * @version Release: 2.0.0RC1 + */ +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 + * @version Release: 2.0.0RC1 + */ +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 + * @version Release: 2.0.0RC1 + */ +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 + * @version Release: 2.0.0RC1 + */ +class HTTP_Request2_MessageException extends HTTP_Request2_Exception {} ?> \ No newline at end of file diff --git a/extlib/HTTP/Request2/MultipartBody.php b/extlib/HTTP/Request2/MultipartBody.php index d8afd8344c..a7bd948baf 100644 --- a/extlib/HTTP/Request2/MultipartBody.php +++ b/extlib/HTTP/Request2/MultipartBody.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +37,7 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: MultipartBody.php 287306 2009-08-14 15:22:52Z avb $ + * @version SVN: $Id: MultipartBody.php 308322 2011-02-14 13:58:03Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ @@ -50,7 +50,7 @@ * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 * @link http://tools.ietf.org/html/rfc1867 */ class HTTP_Request2_MultipartBody @@ -172,7 +172,7 @@ class HTTP_Request2_MultipartBody while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) { $oldLength = $length; if ($this->_pos[0] < $paramCount) { - $param = sprintf($this->_headerParam, $boundary, + $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); diff --git a/extlib/HTTP/Request2/Observer/Log.php b/extlib/HTTP/Request2/Observer/Log.php index b1a0552780..7865906f65 100644 --- a/extlib/HTTP/Request2/Observer/Log.php +++ b/extlib/HTTP/Request2/Observer/Log.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -38,19 +38,19 @@ * @author David Jean Louis * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Log.php 272593 2009-01-02 16:27:14Z avb $ + * @version SVN: $Id: Log.php 308680 2011-02-25 17:40:17Z avb $ * @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 + * 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. * @@ -87,7 +87,7 @@ require_once 'HTTP/Request2/Exception.php'; * @author David Jean Louis * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 * @link http://pear.php.net/package/HTTP_Request2 */ class HTTP_Request2_Observer_Log implements SplObserver @@ -109,7 +109,7 @@ class HTTP_Request2_Observer_Log implements SplObserver public $events = array( 'connect', 'sentHeaders', - 'sentBodyPart', + 'sentBody', 'receivedHeaders', 'receivedBody', 'disconnect', @@ -134,7 +134,7 @@ class HTTP_Request2_Observer_Log implements SplObserver } if (is_resource($target) || $target instanceof Log) { $this->target = $target; - } elseif (false === ($this->target = @fopen($target, 'w'))) { + } elseif (false === ($this->target = @fopen($target, 'ab'))) { throw new HTTP_Request2_Exception("Unable to open '{$target}'"); } } @@ -143,7 +143,7 @@ class HTTP_Request2_Observer_Log implements SplObserver // update() {{{ /** - * Called when the request notify us of an event. + * Called when the request notifies us of an event. * * @param HTTP_Request2 $subject The HTTP_Request2 instance * @@ -167,8 +167,8 @@ class HTTP_Request2_Observer_Log implements SplObserver $this->log('> ' . $header); } break; - case 'sentBodyPart': - $this->log('> ' . $event['data']); + case 'sentBody': + $this->log('> ' . $event['data'] . ' byte(s) sent'); break; case 'receivedHeaders': $this->log(sprintf('< HTTP/%s %s %s', @@ -189,12 +189,12 @@ class HTTP_Request2_Observer_Log implements SplObserver break; } } - + // }}} // log() {{{ /** - * Log the given message to the configured target. + * Logs the given message to the configured target. * * @param string $message Message to display * diff --git a/extlib/HTTP/Request2/Response.php b/extlib/HTTP/Request2/Response.php index c7c1021fbb..73e9a5dc82 100644 --- a/extlib/HTTP/Request2/Response.php +++ b/extlib/HTTP/Request2/Response.php @@ -6,7 +6,7 @@ * * LICENSE: * - * Copyright (c) 2008, 2009, Alexey Borzov + * Copyright (c) 2008-2011, Alexey Borzov * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,13 +37,13 @@ * @package HTTP_Request2 * @author Alexey Borzov * @license http://opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: Response.php 287948 2009-09-01 17:12:18Z avb $ + * @version SVN: $Id: Response.php 309921 2011-04-03 16:43:02Z avb $ * @link http://pear.php.net/package/HTTP_Request2 */ /** * Exception class for HTTP_Request2 package - */ + */ require_once 'HTTP/Request2/Exception.php'; /** @@ -58,11 +58,11 @@ require_once 'HTTP/Request2/Exception.php'; * $headerLine = read_header_line(); * $response->parseHeaderLine($headerLine); * } while ($headerLine != ''); - * + * * while ($chunk = read_body()) { * $response->appendBody($chunk); * } - * + * * var_dump($response->getHeader(), $response->getCookies(), $response->getBody()); * * @@ -70,7 +70,7 @@ require_once 'HTTP/Request2/Exception.php'; * @category HTTP * @package HTTP_Request2 * @author Alexey Borzov - * @version Release: 0.4.1 + * @version Release: 2.0.0RC1 * @link http://tools.ietf.org/html/rfc2616#section-6 */ class HTTP_Request2_Response @@ -95,6 +95,12 @@ class HTTP_Request2_Response */ 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 @@ -164,7 +170,7 @@ class HTTP_Request2_Response 305 => 'Use Proxy', 307 => 'Temporary Redirect', - // 4xx: Client Error - The request contains bad syntax or cannot be + // 4xx: Client Error - The request contains bad syntax or cannot be // fulfilled 400 => 'Bad Request', 401 => 'Unauthorized', @@ -200,14 +206,18 @@ class HTTP_Request2_Response /** * Constructor, parses the response status line * - * @param string Response status line (e.g. "HTTP/1.1 200 OK") - * @param bool Whether body is still encoded by Content-Encoding - * @throws HTTP_Request2_Exception if status line is invalid according to spec + * @param string Response status line (e.g. "HTTP/1.1 200 OK") + * @param bool Whether body is still encoded by Content-Encoding + * @param string Effective URL of the response + * @throws HTTP_Request2_MessageException if status line is invalid according to spec */ - public function __construct($statusLine, $bodyEncoded = true) + public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null) { if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) { - throw new HTTP_Request2_Exception("Malformed response: {$statusLine}"); + throw new HTTP_Request2_MessageException( + "Malformed response: {$statusLine}", + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); } $this->version = $m[1]; $this->code = intval($m[2]); @@ -216,13 +226,14 @@ class HTTP_Request2_Response } elseif (!empty(self::$phrases[$this->code])) { $this->reasonPhrase = self::$phrases[$this->code]; } - $this->bodyEncoded = (bool)$bodyEncoded; + $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 + * 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. @@ -264,7 +275,7 @@ class HTTP_Request2_Response } $this->lastHeader = $name; - // string + // continuation of a previous header } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) { if (!is_array($this->headers[$this->lastHeader])) { $this->headers[$this->lastHeader] .= ' ' . trim($m[1]); @@ -273,13 +284,13 @@ class HTTP_Request2_Response $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]); } } - } + } /** * Parses a Set-Cookie header to fill $cookies array * * @param string value of Set-Cookie header - * @link http://cgi.netscape.com/newsref/std/cookie_spec.html + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html */ protected function parseCookie($cookieString) { @@ -334,9 +345,22 @@ class HTTP_Request2_Response $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 + * @return integer */ public function getStatus() { @@ -352,6 +376,16 @@ class HTTP_Request2_Response 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 * @@ -388,7 +422,7 @@ class HTTP_Request2_Response */ public function getBody() { - if (!$this->bodyEncoded || + if (0 == strlen($this->body) || !$this->bodyEncoded || !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate')) ) { return $this->body; @@ -424,7 +458,7 @@ class HTTP_Request2_Response * Get the HTTP version of the response * * @return string - */ + */ public function getVersion() { return $this->version; @@ -439,7 +473,8 @@ class HTTP_Request2_Response * * @param string gzip-encoded data * @return string decoded data - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_MessageException * @link http://tools.ietf.org/html/rfc1952 */ public static function decodeGzip($data) @@ -450,15 +485,24 @@ class HTTP_Request2_Response return $data; } if (!function_exists('gzinflate')) { - throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available'); + 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_Exception('Error parsing gzip header: unknown compression 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_Exception('Error parsing gzip header: reserved bits are set'); + 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. @@ -466,45 +510,69 @@ class HTTP_Request2_Response // extra fields, need to skip 'em if ($flags & 4) { if ($length - $headerLength - 2 < 8) { - throw new HTTP_Request2_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Error parsing gzip header: data too short'); + 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_Exception('Header CRC check failed'); + throw new HTTP_Request2_MessageException( + 'Header CRC check failed', + HTTP_Request2_Exception::DECODE_ERROR + ); } $headerLength += 2; } @@ -517,11 +585,20 @@ class HTTP_Request2_Response // don't pass $dataSize to gzinflate, see bugs #13135, #14370 $unpacked = gzinflate(substr($data, $headerLength, -8)); if (false === $unpacked) { - throw new HTTP_Request2_Exception('gzinflate() call failed'); + throw new HTTP_Request2_MessageException( + 'gzinflate() call failed', + HTTP_Request2_Exception::DECODE_ERROR + ); } elseif ($dataSize != strlen($unpacked)) { - throw new HTTP_Request2_Exception('Data size check failed'); + 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'); + throw new HTTP_Request2_Exception( + 'Data CRC check failed', + HTTP_Request2_Exception::DECODE_ERROR + ); } return $unpacked; } @@ -531,12 +608,15 @@ class HTTP_Request2_Response * * @param string deflate-encoded data * @return string decoded data - * @throws HTTP_Request2_Exception + * @throws HTTP_Request2_LogicException */ public static function decodeDeflate($data) { if (!function_exists('gzuncompress')) { - throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available'); + 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.