<?php\r
/**\r
- * Class representing a HTTP request\r
+ * Class representing a HTTP request message\r
*\r
* PHP version 5\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Request2.php 278226 2009-04-03 21:32:48Z avb $\r
+ * @version SVN: $Id: Request2.php 308735 2011-02-27 20:31:28Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
\r
/**\r
* Exception class for HTTP_Request2 package\r
- */ \r
+ */\r
require_once 'HTTP/Request2/Exception.php';\r
\r
/**\r
- * Class representing a HTTP request\r
+ * Class representing a HTTP request message\r
*\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
* @link http://tools.ietf.org/html/rfc2616#section-5\r
*/\r
class HTTP_Request2 implements SplSubject\r
/**#@-*/\r
\r
/**#@+\r
- * Constants for HTTP authentication schemes \r
+ * Constants for HTTP authentication schemes\r
*\r
* @link http://tools.ietf.org/html/rfc2617\r
*/\r
/**\r
* Regular expression used to check for invalid symbols in cookie strings\r
* @link http://pear.php.net/bugs/bug.php?id=15630\r
- * @link http://cgi.netscape.com/newsref/std/cookie_spec.html\r
+ * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html\r
*/\r
const REGEXP_INVALID_COOKIE = '/[\s,;]/';\r
\r
'ssl_local_cert' => null,\r
'ssl_passphrase' => null,\r
\r
- 'digest_compat_ie' => false\r
+ 'digest_compat_ie' => false,\r
+\r
+ 'follow_redirects' => false,\r
+ 'max_redirects' => 5,\r
+ 'strict_redirects' => false\r
);\r
\r
/**\r
protected $postParams = array();\r
\r
/**\r
- * Array of file uploads (for multipart/form-data POST requests) \r
+ * Array of file uploads (for multipart/form-data POST requests)\r
* @var array\r
*/\r
protected $uploads = array();\r
*/\r
protected $adapter;\r
\r
+ /**\r
+ * Cookie jar to persist cookies between requests\r
+ * @var HTTP_Request2_CookieJar\r
+ */\r
+ protected $cookieJar = null;\r
\r
/**\r
* Constructor. Can set request URL, method and configuration array.\r
*\r
- * Also sets a default value for User-Agent header. \r
+ * Also sets a default value for User-Agent header.\r
*\r
* @param string|Net_Url2 Request URL\r
* @param string Request method\r
*/\r
public function __construct($url = null, $method = self::METHOD_GET, array $config = array())\r
{\r
+ $this->setConfig($config);\r
if (!empty($url)) {\r
$this->setUrl($url);\r
}\r
if (!empty($method)) {\r
$this->setMethod($method);\r
}\r
- $this->setConfig($config);\r
- $this->setHeader('user-agent', 'HTTP_Request2/0.4.1 ' .\r
+ $this->setHeader('user-agent', 'HTTP_Request2/2.0.0RC1 ' .\r
'(http://pear.php.net/package/http_request2) ' .\r
'PHP/' . phpversion());\r
}\r
*\r
* @param string|Net_URL2 Request URL\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
public function setUrl($url)\r
{\r
if (is_string($url)) {\r
- $url = new Net_URL2($url);\r
+ $url = new Net_URL2(\r
+ $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets'])\r
+ );\r
}\r
if (!$url instanceof Net_URL2) {\r
- throw new HTTP_Request2_Exception('Parameter is not a valid HTTP URL');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Parameter is not a valid HTTP URL',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
}\r
// URL contains username / password?\r
if ($url->getUserinfo()) {\r
*\r
* @param string\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception if the method name is invalid\r
+ * @throws HTTP_Request2_LogicException if the method name is invalid\r
*/\r
public function setMethod($method)\r
{\r
// Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1\r
if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) {\r
- throw new HTTP_Request2_Exception("Invalid request method '{$method}'");\r
+ throw new HTTP_Request2_LogicException(\r
+ "Invalid request method '{$method}'",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
}\r
$this->method = $method;\r
\r
* <li> 'adapter' - adapter to use (string)</li>\r
* <li> 'connect_timeout' - Connection timeout in seconds (integer)</li>\r
* <li> 'timeout' - Total number of seconds a request can take.\r
- * Use 0 for no limit, should be greater than \r
+ * Use 0 for no limit, should be greater than\r
* 'connect_timeout' if set (integer)</li>\r
* <li> 'use_brackets' - Whether to append [] to array variable names (bool)</li>\r
* <li> 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)</li>\r
* certificate matches host name (bool)</li>\r
* <li> 'ssl_cafile' - Cerificate Authority file to verify the peer\r
* with (use with 'ssl_verify_peer') (string)</li>\r
- * <li> 'ssl_capath' - Directory holding multiple Certificate \r
+ * <li> 'ssl_capath' - Directory holding multiple Certificate\r
* Authority files (string)</li>\r
* <li> 'ssl_local_cert' - Name of a file containing local cerificate (string)</li>\r
* <li> 'ssl_passphrase' - Passphrase with which local certificate\r
* <li> 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6\r
* in using URL without query string in digest\r
* authentication (boolean)</li>\r
+ * <li> 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)</li>\r
+ * <li> 'max_redirects' - Maximum number of redirects to follow (integer)</li>\r
+ * <li> 'strict_redirects' - Whether to keep request method on redirects via status 301 and\r
+ * 302 (true, needed for compatibility with RFC 2616)\r
+ * or switch to GET (false, needed for compatibility with most\r
+ * browsers) (boolean)</li>\r
* </ul>\r
*\r
* @param string|array configuration parameter name or array\r
* ('parameter name' => 'parameter value')\r
* @param mixed parameter value if $nameOrConfig is not an array\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception If the parameter is unknown\r
+ * @throws HTTP_Request2_LogicException If the parameter is unknown\r
*/\r
public function setConfig($nameOrConfig, $value = null)\r
{\r
\r
} else {\r
if (!array_key_exists($nameOrConfig, $this->config)) {\r
- throw new HTTP_Request2_Exception(\r
- "Unknown configuration parameter '{$nameOrConfig}'"\r
+ throw new HTTP_Request2_LogicException(\r
+ "Unknown configuration parameter '{$nameOrConfig}'",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
);\r
}\r
$this->config[$nameOrConfig] = $value;\r
* Returns the value(s) of the configuration parameter(s)\r
*\r
* @param string parameter name\r
- * @return mixed value of $name parameter, array of all configuration \r
+ * @return mixed value of $name parameter, array of all configuration\r
* parameters if $name is not given\r
- * @throws HTTP_Request2_Exception If the parameter is unknown\r
+ * @throws HTTP_Request2_LogicException If the parameter is unknown\r
*/\r
public function getConfig($name = null)\r
{\r
if (null === $name) {\r
return $this->config;\r
} elseif (!array_key_exists($name, $this->config)) {\r
- throw new HTTP_Request2_Exception(\r
- "Unknown configuration parameter '{$name}'"\r
+ throw new HTTP_Request2_LogicException(\r
+ "Unknown configuration parameter '{$name}'",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
);\r
}\r
return $this->config[$name];\r
* @param string password\r
* @param string authentication scheme\r
* @return HTTP_Request2\r
- */ \r
+ */\r
public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)\r
{\r
if (empty($user)) {\r
* Sets request header(s)\r
*\r
* The first parameter may be either a full header string 'header: value' or\r
- * header name. In the former case $value parameter is ignored, in the latter \r
+ * header name. In the former case $value parameter is ignored, in the latter\r
* the header's value will either be set to $value or the header will be\r
* removed if $value is null. The first parameter can also be an array of\r
* headers, in that case method will be called recursively.\r
*\r
* Note that headers are treated case insensitively as per RFC 2616.\r
- * \r
+ *\r
* <code>\r
* $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'\r
* $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'\r
*\r
* @param string|array header name, header string ('Header: value')\r
* or an array of headers\r
- * @param string|null header value, header will be removed if null\r
+ * @param string|array|null header value if $name is not an array,\r
+ * header will be removed if value is null\r
+ * @param bool whether to replace previous header with the\r
+ * same name or append to its value\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
- public function setHeader($name, $value = null)\r
+ public function setHeader($name, $value = null, $replace = true)\r
{\r
if (is_array($name)) {\r
foreach ($name as $k => $v) {\r
if (is_string($k)) {\r
- $this->setHeader($k, $v);\r
+ $this->setHeader($k, $v, $replace);\r
} else {\r
- $this->setHeader($v);\r
+ $this->setHeader($v, null, $replace);\r
}\r
}\r
} else {\r
}\r
// Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2\r
if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) {\r
- throw new HTTP_Request2_Exception("Invalid header name '{$name}'");\r
+ throw new HTTP_Request2_LogicException(\r
+ "Invalid header name '{$name}'",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
}\r
// Header names are case insensitive anyway\r
$name = strtolower($name);\r
if (null === $value) {\r
unset($this->headers[$name]);\r
+\r
} else {\r
- $this->headers[$name] = $value;\r
+ if (is_array($value)) {\r
+ $value = implode(', ', array_map('trim', $value));\r
+ } elseif (is_string($value)) {\r
+ $value = trim($value);\r
+ }\r
+ if (!isset($this->headers[$name]) || $replace) {\r
+ $this->headers[$name] = $value;\r
+ } else {\r
+ $this->headers[$name] .= ', ' . $value;\r
+ }\r
}\r
}\r
- \r
+\r
return $this;\r
}\r
\r
}\r
\r
/**\r
- * Appends a cookie to "Cookie:" header\r
+ * Adds a cookie to the request\r
+ *\r
+ * If the request does not have a CookieJar object set, this method simply\r
+ * appends a cookie to "Cookie:" header.\r
+ *\r
+ * If a CookieJar object is available, the cookie is stored in that object.\r
+ * Data from request URL will be used for setting its 'domain' and 'path'\r
+ * parameters, 'expires' and 'secure' will be set to null and false,\r
+ * respectively. If you need further control, use CookieJar's methods.\r
*\r
* @param string cookie name\r
* @param string cookie value\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
+ * @see setCookieJar()\r
*/\r
public function addCookie($name, $value)\r
{\r
- $cookie = $name . '=' . $value;\r
- if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {\r
- throw new HTTP_Request2_Exception("Invalid cookie: '{$cookie}'");\r
+ if (!empty($this->cookieJar)) {\r
+ $this->cookieJar->store(array('name' => $name, 'value' => $value),\r
+ $this->url);\r
+\r
+ } else {\r
+ $cookie = $name . '=' . $value;\r
+ if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {\r
+ throw new HTTP_Request2_LogicException(\r
+ "Invalid cookie: '{$cookie}'",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
+ }\r
+ $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';\r
+ $this->setHeader('cookie', $cookies . $cookie);\r
}\r
- $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';\r
- $this->setHeader('cookie', $cookies . $cookie);\r
\r
return $this;\r
}\r
/**\r
* Sets the request body\r
*\r
- * @param string Either a string with the body or filename containing body\r
+ * If you provide file pointer rather than file name, it should support\r
+ * fstat() and rewind() operations.\r
+ *\r
+ * @param string|resource|HTTP_Request2_MultipartBody Either a string\r
+ * with the body or filename containing body or pointer to\r
+ * an open file or object with multipart body data\r
* @param bool Whether first parameter is a filename\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
public function setBody($body, $isFilename = false)\r
{\r
- if (!$isFilename) {\r
- $this->body = (string)$body;\r
- } else {\r
- if (!($fp = @fopen($body, 'rb'))) {\r
- throw new HTTP_Request2_Exception("Cannot open file {$body}");\r
+ if (!$isFilename && !is_resource($body)) {\r
+ if (!$body instanceof HTTP_Request2_MultipartBody) {\r
+ $this->body = (string)$body;\r
+ } else {\r
+ $this->body = $body;\r
}\r
- $this->body = $fp;\r
+ } else {\r
+ $fileData = $this->fopenWrapper($body, empty($this->headers['content-type']));\r
+ $this->body = $fileData['fp'];\r
if (empty($this->headers['content-type'])) {\r
- $this->setHeader('content-type', self::detectMimeType($body));\r
+ $this->setHeader('content-type', $fileData['type']);\r
}\r
}\r
+ $this->postParams = $this->uploads = array();\r
\r
return $this;\r
}\r
*/\r
public function getBody()\r
{\r
- if (self::METHOD_POST == $this->method && \r
+ if (self::METHOD_POST == $this->method &&\r
(!empty($this->postParams) || !empty($this->uploads))\r
) {\r
- if ('application/x-www-form-urlencoded' == $this->headers['content-type']) {\r
+ if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) {\r
$body = http_build_query($this->postParams, '', '&');\r
if (!$this->getConfig('use_brackets')) {\r
$body = preg_replace('/%5B\d+%5D=/', '=', $body);\r
// support RFC 3986 by not encoding '~' symbol (request #15368)\r
return str_replace('%7E', '~', $body);\r
\r
- } elseif ('multipart/form-data' == $this->headers['content-type']) {\r
+ } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) {\r
require_once 'HTTP/Request2/MultipartBody.php';\r
return new HTTP_Request2_MultipartBody(\r
$this->postParams, $this->uploads, $this->getConfig('use_brackets')\r
* If you just want to send the contents of a file as the body of HTTP\r
* request you should use setBody() method.\r
*\r
+ * If you provide file pointers rather than file names, they should support\r
+ * fstat() and rewind() operations.\r
+ *\r
* @param string name of file-upload field\r
- * @param mixed full name of local file\r
- * @param string filename to send in the request \r
+ * @param string|resource|array full name of local file, pointer to\r
+ * open file or an array of files\r
+ * @param string filename to send in the request\r
* @param string content-type of file being uploaded\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
public function addUpload($fieldName, $filename, $sendFilename = null,\r
$contentType = null)\r
{\r
if (!is_array($filename)) {\r
- if (!($fp = @fopen($filename, 'rb'))) {\r
- throw new HTTP_Request2_Exception("Cannot open file {$filename}");\r
- }\r
+ $fileData = $this->fopenWrapper($filename, empty($contentType));\r
$this->uploads[$fieldName] = array(\r
- 'fp' => $fp,\r
- 'filename' => empty($sendFilename)? basename($filename): $sendFilename,\r
- 'size' => filesize($filename),\r
- 'type' => empty($contentType)? self::detectMimeType($filename): $contentType\r
+ 'fp' => $fileData['fp'],\r
+ 'filename' => !empty($sendFilename)? $sendFilename\r
+ :(is_string($filename)? basename($filename): 'anonymous.blob') ,\r
+ 'size' => $fileData['size'],\r
+ 'type' => empty($contentType)? $fileData['type']: $contentType\r
);\r
} else {\r
$fps = $names = $sizes = $types = array();\r
if (!is_array($f)) {\r
$f = array($f);\r
}\r
- if (!($fp = @fopen($f[0], 'rb'))) {\r
- throw new HTTP_Request2_Exception("Cannot open file {$f[0]}");\r
- }\r
- $fps[] = $fp;\r
- $names[] = empty($f[1])? basename($f[0]): $f[1];\r
- $sizes[] = filesize($f[0]);\r
- $types[] = empty($f[2])? self::detectMimeType($f[0]): $f[2];\r
+ $fileData = $this->fopenWrapper($f[0], empty($f[2]));\r
+ $fps[] = $fileData['fp'];\r
+ $names[] = !empty($f[1])? $f[1]\r
+ :(is_string($f[0])? basename($f[0]): 'anonymous.blob');\r
+ $sizes[] = $fileData['size'];\r
+ $types[] = empty($f[2])? $fileData['type']: $f[2];\r
}\r
$this->uploads[$fieldName] = array(\r
'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types\r
* <li>'disconnect' - after disconnection from server</li>\r
* <li>'sentHeaders' - after sending the request headers,\r
* data is the headers sent (string)</li>\r
- * <li>'sentBodyPart' - after sending a part of the request body, \r
+ * <li>'sentBodyPart' - after sending a part of the request body,\r
* data is the length of that part (int)</li>\r
+ * <li>'sentBody' - after sending the whole request body,\r
+ * data is request body length (int)</li>\r
* <li>'receivedHeaders' - after receiving the response headers,\r
* data is HTTP_Request2_Response object</li>\r
* <li>'receivedBodyPart' - after receiving a part of the response\r
*\r
* @param string|HTTP_Request2_Adapter\r
* @return HTTP_Request2\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
public function setAdapter($adapter)\r
{\r
include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php';\r
}\r
if (!class_exists($adapter, false)) {\r
- throw new HTTP_Request2_Exception("Class {$adapter} not found");\r
+ throw new HTTP_Request2_LogicException(\r
+ "Class {$adapter} not found",\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
}\r
}\r
$adapter = new $adapter;\r
}\r
if (!$adapter instanceof HTTP_Request2_Adapter) {\r
- throw new HTTP_Request2_Exception('Parameter is not a HTTP request adapter');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Parameter is not a HTTP request adapter',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
}\r
$this->adapter = $adapter;\r
\r
return $this;\r
}\r
\r
+ /**\r
+ * Sets the cookie jar\r
+ *\r
+ * A cookie jar is used to maintain cookies across HTTP requests and\r
+ * responses. Cookies from jar will be automatically added to the request\r
+ * headers based on request URL.\r
+ *\r
+ * @param HTTP_Request2_CookieJar|bool Existing CookieJar object, true to\r
+ * create a new one, false to remove\r
+ */\r
+ public function setCookieJar($jar = true)\r
+ {\r
+ if (!class_exists('HTTP_Request2_CookieJar', false)) {\r
+ require_once 'HTTP/Request2/CookieJar.php';\r
+ }\r
+\r
+ if ($jar instanceof HTTP_Request2_CookieJar) {\r
+ $this->cookieJar = $jar;\r
+ } elseif (true === $jar) {\r
+ $this->cookieJar = new HTTP_Request2_CookieJar();\r
+ } elseif (!$jar) {\r
+ $this->cookieJar = null;\r
+ } else {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Invalid parameter passed to setCookieJar()',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
+ }\r
+\r
+ return $this;\r
+ }\r
+\r
+ /**\r
+ * Returns current CookieJar object or null if none\r
+ *\r
+ * @return HTTP_Request2_CookieJar|null\r
+ */\r
+ public function getCookieJar()\r
+ {\r
+ return $this->cookieJar;\r
+ }\r
+\r
/**\r
* Sends the request and returns the response\r
*\r
public function send()\r
{\r
// Sanity check for URL\r
- if (!$this->url instanceof Net_URL2) {\r
- throw new HTTP_Request2_Exception('No URL given');\r
- } elseif (!$this->url->isAbsolute()) {\r
- throw new HTTP_Request2_Exception('Absolute URL required');\r
- } elseif (!in_array(strtolower($this->url->getScheme()), array('https', 'http'))) {\r
- throw new HTTP_Request2_Exception('Not a HTTP URL');\r
+ if (!$this->url instanceof Net_URL2\r
+ || !$this->url->isAbsolute()\r
+ || !in_array(strtolower($this->url->getScheme()), array('https', 'http'))\r
+ ) {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'HTTP_Request2 needs an absolute HTTP(S) request URL, '\r
+ . ($this->url instanceof Net_URL2\r
+ ? 'none' : "'" . $this->url->__toString() . "'")\r
+ . ' given',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
}\r
if (empty($this->adapter)) {\r
$this->setAdapter($this->getConfig('adapter'));\r
}\r
// magic_quotes_runtime may break file uploads and chunked response\r
- // processing; see bug #4543\r
- if ($magicQuotes = ini_get('magic_quotes_runtime')) {\r
- ini_set('magic_quotes_runtime', false);\r
+ // processing; see bug #4543. Don't use ini_get() here; see bug #16440.\r
+ if ($magicQuotes = get_magic_quotes_runtime()) {\r
+ set_magic_quotes_runtime(false);\r
}\r
// force using single byte encoding if mbstring extension overloads\r
// strlen() and substr(); see bug #1781, bug #10605\r
}\r
// cleanup in either case (poor man's "finally" clause)\r
if ($magicQuotes) {\r
- ini_set('magic_quotes_runtime', true);\r
+ set_magic_quotes_runtime(true);\r
}\r
if (!empty($oldEncoding)) {\r
mb_internal_encoding($oldEncoding);\r
return $response;\r
}\r
\r
+ /**\r
+ * Wrapper around fopen()/fstat() used by setBody() and addUpload()\r
+ *\r
+ * @param string|resource file name or pointer to open file\r
+ * @param bool whether to try autodetecting MIME type of file,\r
+ * will only work if $file is a filename, not pointer\r
+ * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type)\r
+ * @throws HTTP_Request2_LogicException\r
+ */\r
+ protected function fopenWrapper($file, $detectType = false)\r
+ {\r
+ if (!is_string($file) && !is_resource($file)) {\r
+ throw new HTTP_Request2_LogicException(\r
+ "Filename or file pointer resource expected",\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT\r
+ );\r
+ }\r
+ $fileData = array(\r
+ 'fp' => is_string($file)? null: $file,\r
+ 'type' => 'application/octet-stream',\r
+ 'size' => 0\r
+ );\r
+ if (is_string($file)) {\r
+ $track = @ini_set('track_errors', 1);\r
+ if (!($fileData['fp'] = @fopen($file, 'rb'))) {\r
+ $e = new HTTP_Request2_LogicException(\r
+ $php_errormsg, HTTP_Request2_Exception::READ_ERROR\r
+ );\r
+ }\r
+ @ini_set('track_errors', $track);\r
+ if (isset($e)) {\r
+ throw $e;\r
+ }\r
+ if ($detectType) {\r
+ $fileData['type'] = self::detectMimeType($file);\r
+ }\r
+ }\r
+ if (!($stat = fstat($fileData['fp']))) {\r
+ throw new HTTP_Request2_LogicException(\r
+ "fstat() call failed", HTTP_Request2_Exception::READ_ERROR\r
+ );\r
+ }\r
+ $fileData['size'] = $stat['size'];\r
+\r
+ return $fileData;\r
+ }\r
+\r
/**\r
* Tries to detect MIME type of a file\r
*\r
*/\r
protected static function detectMimeType($filename)\r
{\r
- // finfo extension from PECL available \r
+ // finfo extension from PECL available\r
if (function_exists('finfo_open')) {\r
if (!isset(self::$_fileinfoDb)) {\r
self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);\r
}\r
- if (self::$_fileinfoDb) { \r
+ if (self::$_fileinfoDb) {\r
$info = finfo_file(self::$_fileinfoDb, $filename);\r
}\r
}\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Adapter.php 274684 2009-01-26 23:07:27Z avb $\r
+ * @version SVN: $Id: Adapter.php 308322 2011-02-14 13:58:03Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* Base class for HTTP_Request2 adapters\r
*\r
* HTTP_Request2 class itself only defines methods for aggregating the request\r
- * data, all actual work of sending the request to the remote server and \r
+ * data, all actual work of sending the request to the remote server and\r
* receiving its response is performed by adapters.\r
*\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
abstract class HTTP_Request2_Adapter\r
{\r
/**\r
* Calculates length of the request body, adds proper headers\r
*\r
- * @param array associative array of request headers, this method will \r
- * add proper 'Content-Length' and 'Content-Type' headers \r
+ * @param array associative array of request headers, this method will\r
+ * add proper 'Content-Length' and 'Content-Type' headers\r
* to this array (or remove them if not needed)\r
*/\r
protected function calculateRequestLength(&$headers)\r
if (in_array($this->request->getMethod(), self::$bodyDisallowed) ||\r
0 == $this->contentLength\r
) {\r
- unset($headers['content-type']);\r
// No body: send a Content-Length header nonetheless (request #12900),\r
// but do that only for methods that require a body (bug #14740)\r
if (in_array($this->request->getMethod(), self::$bodyRequired)) {\r
$headers['content-length'] = 0;\r
} else {\r
unset($headers['content-length']);\r
+ // if the method doesn't require a body and doesn't have a\r
+ // body, don't send a Content-Type header. (request #16799)\r
+ unset($headers['content-type']);\r
}\r
} else {\r
if (empty($headers['content-type'])) {\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Curl.php 278226 2009-04-03 21:32:48Z avb $\r
+ * @version SVN: $Id: Curl.php 310800 2011-05-06 07:29:56Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter\r
{\r
'ssl_passphrase' => CURLOPT_SSLCERTPASSWD\r
);\r
\r
+ /**\r
+ * Mapping of CURLE_* constants to Exception subclasses and error codes\r
+ * @var array\r
+ */\r
+ protected static $errorMap = array(\r
+ CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),\r
+ // error returned from write callback\r
+ CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::TIMEOUT),\r
+ CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT),\r
+ CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',\r
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS),\r
+ CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::MISCONFIGURATION),\r
+ CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),\r
+ CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',\r
+ HTTP_Request2_Exception::INVALID_ARGUMENT),\r
+ CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),\r
+ CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),\r
+ );\r
+\r
/**\r
* Response being received\r
* @var HTTP_Request2_Response\r
*/\r
protected $lastInfo;\r
\r
+ /**\r
+ * Creates a subclass of HTTP_Request2_Exception from curl error data\r
+ *\r
+ * @param resource curl handle\r
+ * @return HTTP_Request2_Exception\r
+ */\r
+ protected static function wrapCurlError($ch)\r
+ {\r
+ $nativeCode = curl_errno($ch);\r
+ $message = 'Curl error: ' . curl_error($ch);\r
+ if (!isset(self::$errorMap[$nativeCode])) {\r
+ return new HTTP_Request2_Exception($message, 0, $nativeCode);\r
+ } else {\r
+ $class = self::$errorMap[$nativeCode][0];\r
+ $code = empty(self::$errorMap[$nativeCode][1])\r
+ ? 0 : self::$errorMap[$nativeCode][1];\r
+ return new $class($message, $code, $nativeCode);\r
+ }\r
+ }\r
+\r
/**\r
* Sends request to the remote server and returns its response\r
*\r
public function sendRequest(HTTP_Request2 $request)\r
{\r
if (!extension_loaded('curl')) {\r
- throw new HTTP_Request2_Exception('cURL extension not available');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
}\r
\r
$this->request = $request;\r
\r
try {\r
if (false === curl_exec($ch = $this->createCurlHandle())) {\r
- $errorMessage = 'Error sending request: #' . curl_errno($ch) .\r
- ' ' . curl_error($ch);\r
+ $e = self::wrapCurlError($ch);\r
}\r
} catch (Exception $e) {\r
}\r
- $this->lastInfo = curl_getinfo($ch);\r
- curl_close($ch);\r
+ if (isset($ch)) {\r
+ $this->lastInfo = curl_getinfo($ch);\r
+ curl_close($ch);\r
+ }\r
+\r
+ $response = $this->response;\r
+ unset($this->request, $this->requestBody, $this->response);\r
\r
if (!empty($e)) {\r
throw $e;\r
- } elseif (!empty($errorMessage)) {\r
- throw new HTTP_Request2_Exception($errorMessage);\r
+ }\r
+\r
+ if ($jar = $request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($response, $request->getUrl());\r
}\r
\r
if (0 < $this->lastInfo['size_download']) {\r
- $this->request->setLastEvent('receivedBody', $this->response);\r
+ $request->setLastEvent('receivedBody', $response);\r
}\r
- return $this->response;\r
+ return $response;\r
}\r
\r
/**\r
* Creates a new cURL handle and populates it with data from the request\r
*\r
* @return resource a cURL handle, as created by curl_init()\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
protected function createCurlHandle()\r
{\r
$ch = curl_init();\r
\r
curl_setopt_array($ch, array(\r
- // setup callbacks\r
- CURLOPT_READFUNCTION => array($this, 'callbackReadBody'),\r
+ // setup write callbacks\r
CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),\r
CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),\r
- // disallow redirects\r
- CURLOPT_FOLLOWLOCATION => false,\r
// buffer size\r
CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),\r
// connection timeout\r
CURLOPT_URL => $this->request->getUrl()->getUrl()\r
));\r
\r
+ // set up redirects\r
+ if (!$this->request->getConfig('follow_redirects')) {\r
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);\r
+ } else {\r
+ if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
+ }\r
+ curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));\r
+ // limit redirects to http(s), works in 5.2.10+\r
+ if (defined('CURLOPT_REDIR_PROTOCOLS')) {\r
+ curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);\r
+ }\r
+ // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571\r
+ if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {\r
+ curl_setopt($ch, CURLOPT_POSTREDIR, 3);\r
+ }\r
+ }\r
+\r
// request timeout\r
if ($timeout = $this->request->getConfig('timeout')) {\r
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);\r
case HTTP_Request2::METHOD_POST:\r
curl_setopt($ch, CURLOPT_POST, true);\r
break;\r
+ case HTTP_Request2::METHOD_HEAD:\r
+ curl_setopt($ch, CURLOPT_NOBODY, true);\r
+ break;\r
+ case HTTP_Request2::METHOD_PUT:\r
+ curl_setopt($ch, CURLOPT_UPLOAD, true);\r
+ break;\r
default:\r
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());\r
}\r
// set proxy, if needed\r
if ($host = $this->request->getConfig('proxy_host')) {\r
if (!($port = $this->request->getConfig('proxy_port'))) {\r
- throw new HTTP_Request2_Exception('Proxy port not provided');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
}\r
curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);\r
if ($user = $this->request->getConfig('proxy_user')) {\r
}\r
\r
// set SSL options\r
- if (0 == strcasecmp($this->request->getUrl()->getScheme(), 'https')) {\r
- foreach ($this->request->getConfig() as $name => $value) {\r
- if ('ssl_verify_host' == $name && null !== $value) {\r
- curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
- } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
- curl_setopt($ch, self::$sslContextMap[$name], $value);\r
- }\r
+ foreach ($this->request->getConfig() as $name => $value) {\r
+ if ('ssl_verify_host' == $name && null !== $value) {\r
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);\r
+ } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {\r
+ curl_setopt($ch, self::$sslContextMap[$name], $value);\r
}\r
}\r
\r
$headers['accept-encoding'] = '';\r
}\r
\r
+ if (($jar = $this->request->getCookieJar())\r
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
+ ) {\r
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
+ }\r
+\r
// set headers having special cURL keys\r
foreach (self::$headerMap as $name => $option) {\r
if (isset($headers[$name])) {\r
}\r
\r
$this->calculateRequestLength($headers);\r
+ if (isset($headers['content-length'])) {\r
+ $this->workaroundPhpBug47204($ch, $headers);\r
+ }\r
\r
// set headers not having special keys\r
$headersFmt = array();\r
return $ch;\r
}\r
\r
+ /**\r
+ * Workaround for PHP bug #47204 that prevents rewinding request body\r
+ *\r
+ * The workaround consists of reading the entire request body into memory\r
+ * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large\r
+ * file uploads, use Socket adapter instead.\r
+ *\r
+ * @param resource cURL handle\r
+ * @param array Request headers\r
+ */\r
+ protected function workaroundPhpBug47204($ch, &$headers)\r
+ {\r
+ // no redirects, no digest auth -> probably no rewind needed\r
+ if (!$this->request->getConfig('follow_redirects')\r
+ && (!($auth = $this->request->getAuth())\r
+ || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])\r
+ ) {\r
+ curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));\r
+\r
+ // rewind may be needed, read the whole body into memory\r
+ } else {\r
+ if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {\r
+ $this->requestBody = $this->requestBody->__toString();\r
+\r
+ } elseif (is_resource($this->requestBody)) {\r
+ $fp = $this->requestBody;\r
+ $this->requestBody = '';\r
+ while (!feof($fp)) {\r
+ $this->requestBody .= fread($fp, 16384);\r
+ }\r
+ }\r
+ // curl hangs up if content-length is present\r
+ unset($headers['content-length']);\r
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);\r
+ }\r
+ }\r
+\r
/**\r
* Callback function called by cURL for reading the request body\r
*\r
* @param resource cURL handle\r
* @param resource file descriptor (not used)\r
* @param integer maximum length of data to return\r
- * @return string part of the request body, up to $length bytes \r
+ * @return string part of the request body, up to $length bytes\r
*/\r
protected function callbackReadBody($ch, $fd, $length)\r
{\r
'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
);\r
}\r
+ $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);\r
+ // if body wasn't read by a callback, send event with total body size\r
+ if ($upload > $this->position) {\r
+ $this->request->setLastEvent(\r
+ 'sentBodyPart', $upload - $this->position\r
+ );\r
+ $this->position = $upload;\r
+ }\r
+ if ($upload && (!$this->eventSentHeaders\r
+ || $this->response->getStatus() >= 200)\r
+ ) {\r
+ $this->request->setLastEvent('sentBody', $upload);\r
+ }\r
$this->eventSentHeaders = true;\r
// we'll need a new response object\r
if ($this->eventReceivedHeaders) {\r
}\r
}\r
if (empty($this->response)) {\r
- $this->response = new HTTP_Request2_Response($string, false);\r
+ $this->response = new HTTP_Request2_Response(\r
+ $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)\r
+ );\r
} else {\r
$this->response->parseHeaderLine($string);\r
if ('' == trim($string)) {\r
if (200 <= $this->response->getStatus()) {\r
$this->request->setLastEvent('receivedHeaders', $this->response);\r
}\r
+\r
+ if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {\r
+ $redirectUrl = new Net_URL2($this->response->getHeader('location'));\r
+\r
+ // for versions lower than 5.2.10, check the redirection URL protocol\r
+ if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()\r
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
+ ) {\r
+ return -1;\r
+ }\r
+\r
+ if ($jar = $this->request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($this->response, $this->request->getUrl());\r
+ if (!$redirectUrl->isAbsolute()) {\r
+ $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);\r
+ }\r
+ if ($cookies = $jar->getMatching($redirectUrl, true)) {\r
+ curl_setopt($ch, CURLOPT_COOKIE, $cookies);\r
+ }\r
+ }\r
+ }\r
$this->eventReceivedHeaders = true;\r
}\r
}\r
*/\r
protected function callbackWriteBody($ch, $string)\r
{\r
- // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if \r
+ // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if\r
// response doesn't start with proper HTTP status line (see bug #15716)\r
if (empty($this->response)) {\r
- throw new HTTP_Request2_Exception("Malformed response: {$string}");\r
+ throw new HTTP_Request2_MessageException(\r
+ "Malformed response: {$string}",\r
+ HTTP_Request2_Exception::MALFORMED_RESPONSE\r
+ );\r
}\r
if ($this->request->getConfig('store_body')) {\r
$this->response->appendBody($string);\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Mock.php 274406 2009-01-23 18:01:57Z avb $\r
+ * @version SVN: $Id: Mock.php 308322 2011-02-14 13:58:03Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* <code>\r
* $mock = new HTTP_Request2_Adapter_Mock();\r
* $mock->addResponse("HTTP/1.1 ... ");\r
- * \r
+ *\r
* $request = new HTTP_Request2();\r
* $request->setAdapter($mock);\r
- * \r
+ *\r
* // This will return the response set above\r
* $response = $req->send();\r
- * </code> \r
+ * </code>\r
*\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter\r
{\r
/**\r
* A queue of responses to be returned by sendRequest()\r
- * @var array \r
+ * @var array\r
*/\r
protected $responses = array();\r
\r
/**\r
* Returns the next response from the queue built by addResponse()\r
*\r
- * If the queue is empty will return default empty response with status 400,\r
+ * If the queue is empty it will return default empty response with status 400,\r
* if an Exception object was added to the queue it will be thrown.\r
*\r
* @param HTTP_Request2\r
if ($response instanceof HTTP_Request2_Response) {\r
return $response;\r
} else {\r
- // rethrow the exception,\r
+ // rethrow the exception\r
$class = get_class($response);\r
$message = $response->getMessage();\r
$code = $response->getCode();\r
* Adds response to the queue\r
*\r
* @param mixed either a string, a pointer to an open file,\r
- * a HTTP_Request2_Response or Exception object\r
+ * an instance of HTTP_Request2_Response or Exception\r
* @throws HTTP_Request2_Exception\r
*/\r
public function addResponse($response)\r
public static function createResponseFromString($str)\r
{\r
$parts = preg_split('!(\r?\n){2}!m', $str, 2);\r
- $headerLines = explode("\n", $parts[0]); \r
+ $headerLines = explode("\n", $parts[0]);\r
$response = new HTTP_Request2_Response(array_shift($headerLines));\r
foreach ($headerLines as $headerLine) {\r
$response->parseHeaderLine($headerLine);\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Socket.php 279760 2009-05-03 10:46:42Z avb $\r
+ * @version SVN: $Id: Socket.php 309921 2011-04-03 16:43:02Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter\r
{\r
/**\r
* Regular expression for 'token' rule from RFC 2616\r
- */ \r
+ */\r
const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';\r
\r
/**\r
/**\r
* Data for digest authentication scheme\r
*\r
- * The keys for the array are URL prefixes. \r
+ * The keys for the array are URL prefixes.\r
*\r
- * The values are associative arrays with data (realm, nonce, nonce-count, \r
- * opaque...) needed for digest authentication. Stored here to prevent making \r
- * duplicate requests to digest-protected resources after we have already \r
+ * The values are associative arrays with data (realm, nonce, nonce-count,\r
+ * opaque...) needed for digest authentication. Stored here to prevent making\r
+ * duplicate requests to digest-protected resources after we have already\r
* received the challenge.\r
*\r
* @var array\r
protected $proxyChallenge;\r
\r
/**\r
- * Global timeout, exception will be raised if request continues past this time\r
+ * Sum of start time and global timeout, exception will be thrown if request continues past this time\r
* @var integer\r
*/\r
- protected $timeout = null;\r
+ protected $deadline = null;\r
\r
/**\r
* Remaining length of the current chunk, when reading chunked response\r
* @var integer\r
* @see readChunked()\r
- */ \r
+ */\r
protected $chunkLength = 0;\r
\r
+ /**\r
+ * Remaining amount of redirections to follow\r
+ *\r
+ * Starts at 'max_redirects' configuration parameter and is reduced on each\r
+ * subsequent redirect. An Exception will be thrown once it reaches zero.\r
+ *\r
+ * @var integer\r
+ */\r
+ protected $redirectCountdown = null;\r
+\r
/**\r
* Sends request to the remote server and returns its response\r
*\r
public function sendRequest(HTTP_Request2 $request)\r
{\r
$this->request = $request;\r
- $keepAlive = $this->connect();\r
- $headers = $this->prepareHeaders();\r
\r
- // Use global request timeout if given, see feature requests #5735, #8964 \r
+ // Use global request timeout if given, see feature requests #5735, #8964\r
if ($timeout = $request->getConfig('timeout')) {\r
- $this->timeout = time() + $timeout;\r
+ $this->deadline = time() + $timeout;\r
} else {\r
- $this->timeout = null;\r
+ $this->deadline = null;\r
}\r
\r
try {\r
+ $keepAlive = $this->connect();\r
+ $headers = $this->prepareHeaders();\r
if (false === @fwrite($this->socket, $headers, strlen($headers))) {\r
- throw new HTTP_Request2_Exception('Error writing request');\r
+ throw new HTTP_Request2_MessageException('Error writing request');\r
}\r
// provide request headers to the observer, see request #7633\r
$this->request->setLastEvent('sentHeaders', $headers);\r
$this->writeBody();\r
\r
- if ($this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $request->getConfig('timeout') . ' second(s)'\r
+ if ($this->deadline && time() > $this->deadline) {\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Request timed out after ' .\r
+ $request->getConfig('timeout') . ' second(s)',\r
+ HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
\r
$response = $this->readResponse();\r
\r
+ if ($jar = $request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($response, $request->getUrl());\r
+ }\r
+\r
if (!$this->canKeepAlive($keepAlive, $response)) {\r
$this->disconnect();\r
}\r
\r
} catch (Exception $e) {\r
$this->disconnect();\r
+ }\r
+\r
+ unset($this->request, $this->requestBody);\r
+\r
+ if (!empty($e)) {\r
+ $this->redirectCountdown = null;\r
throw $e;\r
}\r
\r
- return $response;\r
+ if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {\r
+ $this->redirectCountdown = null;\r
+ return $response;\r
+ } else {\r
+ return $this->handleRedirect($request, $response);\r
+ }\r
}\r
\r
/**\r
\r
if ($host = $this->request->getConfig('proxy_host')) {\r
if (!($port = $this->request->getConfig('proxy_port'))) {\r
- throw new HTTP_Request2_Exception('Proxy port not provided');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Proxy port not provided',\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
}\r
$proxy = true;\r
} else {\r
}\r
\r
if ($tunnel && !$proxy) {\r
- throw new HTTP_Request2_Exception(\r
- "Trying to perform CONNECT request without proxy"\r
+ throw new HTTP_Request2_LogicException(\r
+ "Trying to perform CONNECT request without proxy",\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
);\r
}\r
if ($secure && !in_array('ssl', stream_get_transports())) {\r
- throw new HTTP_Request2_Exception(\r
- 'Need OpenSSL support for https:// requests'\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Need OpenSSL support for https:// requests',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
);\r
}\r
\r
// RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive\r
// connection token to a proxy server...\r
- if ($proxy && !$secure && \r
+ if ($proxy && !$secure &&\r
!empty($headers['connection']) && 'Keep-Alive' == $headers['connection']\r
) {\r
$this->request->setHeader('connection');\r
}\r
\r
- $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && \r
+ $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&\r
empty($headers['connection'])) ||\r
(!empty($headers['connection']) &&\r
'Keep-Alive' == $headers['connection']);\r
$context = stream_context_create();\r
foreach ($options as $name => $value) {\r
if (!stream_context_set_option($context, 'ssl', $name, $value)) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_LogicException(\r
"Error setting SSL context option '{$name}'"\r
);\r
}\r
}\r
+ $track = @ini_set('track_errors', 1);\r
$this->socket = @stream_socket_client(\r
$remote, $errno, $errstr,\r
$this->request->getConfig('connect_timeout'),\r
STREAM_CLIENT_CONNECT, $context\r
);\r
if (!$this->socket) {\r
- throw new HTTP_Request2_Exception(\r
- "Unable to connect to {$remote}. Error #{$errno}: {$errstr}"\r
+ $e = new HTTP_Request2_ConnectionException(\r
+ "Unable to connect to {$remote}. Error: "\r
+ . (empty($errstr)? $php_errormsg: $errstr), 0, $errno\r
);\r
}\r
+ @ini_set('track_errors', $track);\r
+ if (isset($e)) {\r
+ throw $e;\r
+ }\r
$this->request->setLastEvent('connect', $remote);\r
self::$sockets[$socketKey] =& $this->socket;\r
}\r
$response = $connect->send();\r
// Need any successful (2XX) response\r
if (200 > $response->getStatus() || 300 <= $response->getStatus()) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_ConnectionException(\r
'Failed to connect via HTTPS proxy. Proxy response: ' .\r
$response->getStatus() . ' ' . $response->getReasonPhrase()\r
);\r
$this->socket = $donor->socket;\r
\r
$modes = array(\r
- STREAM_CRYPTO_METHOD_TLS_CLIENT, \r
+ STREAM_CRYPTO_METHOD_TLS_CLIENT,\r
STREAM_CRYPTO_METHOD_SSLv3_CLIENT,\r
STREAM_CRYPTO_METHOD_SSLv23_CLIENT,\r
- STREAM_CRYPTO_METHOD_SSLv2_CLIENT \r
+ STREAM_CRYPTO_METHOD_SSLv2_CLIENT\r
);\r
\r
foreach ($modes as $mode) {\r
return;\r
}\r
}\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_ConnectionException(\r
'Failed to enable secure connection when connecting through proxy'\r
);\r
}\r
/**\r
* Checks whether current connection may be reused or should be closed\r
*\r
- * @param boolean whether connection could be persistent \r
+ * @param boolean whether connection could be persistent\r
* in the first place\r
* @param HTTP_Request2_Response response object to check\r
* @return boolean\r
return true;\r
}\r
\r
- $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) ||\r
- null !== $response->getHeader('content-length');\r
+ $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))\r
+ || null !== $response->getHeader('content-length')\r
+ // no body possible for such responses, see also request #17031\r
+ || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()\r
+ || in_array($response->getStatus(), array(204, 304));\r
$persistent = 'keep-alive' == strtolower($response->getHeader('connection')) ||\r
(null === $response->getHeader('connection') &&\r
'1.1' == $response->getVersion());\r
}\r
}\r
\r
+ /**\r
+ * Handles HTTP redirection\r
+ *\r
+ * This method will throw an Exception if redirect to a non-HTTP(S) location\r
+ * is attempted, also if number of redirects performed already is equal to\r
+ * 'max_redirects' configuration parameter.\r
+ *\r
+ * @param HTTP_Request2 Original request\r
+ * @param HTTP_Request2_Response Response containing redirect\r
+ * @return HTTP_Request2_Response Response from a new location\r
+ * @throws HTTP_Request2_Exception\r
+ */\r
+ protected function handleRedirect(HTTP_Request2 $request,\r
+ HTTP_Request2_Response $response)\r
+ {\r
+ if (is_null($this->redirectCountdown)) {\r
+ $this->redirectCountdown = $request->getConfig('max_redirects');\r
+ }\r
+ if (0 == $this->redirectCountdown) {\r
+ $this->redirectCountdown = null;\r
+ // Copying cURL behaviour\r
+ throw new HTTP_Request2_MessageException (\r
+ 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',\r
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS\r
+ );\r
+ }\r
+ $redirectUrl = new Net_URL2(\r
+ $response->getHeader('location'),\r
+ array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))\r
+ );\r
+ // refuse non-HTTP redirect\r
+ if ($redirectUrl->isAbsolute()\r
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
+ ) {\r
+ $this->redirectCountdown = null;\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT\r
+ );\r
+ }\r
+ // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),\r
+ // but in practice it is often not\r
+ if (!$redirectUrl->isAbsolute()) {\r
+ $redirectUrl = $request->getUrl()->resolve($redirectUrl);\r
+ }\r
+ $redirect = clone $request;\r
+ $redirect->setUrl($redirectUrl);\r
+ if (303 == $response->getStatus() || (!$request->getConfig('strict_redirects')\r
+ && in_array($response->getStatus(), array(301, 302)))\r
+ ) {\r
+ $redirect->setMethod(HTTP_Request2::METHOD_GET);\r
+ $redirect->setBody('');\r
+ }\r
+\r
+ if (0 < $this->redirectCountdown) {\r
+ $this->redirectCountdown--;\r
+ }\r
+ return $this->sendRequest($redirect);\r
+ }\r
+\r
/**\r
* Checks whether another request should be performed with server digest auth\r
*\r
* - auth credentials should be set in the request object\r
* - response should contain WWW-Authenticate header with digest challenge\r
* - there is either no challenge stored for this URL or new challenge\r
- * contains stale=true parameter (in other case we probably just failed \r
+ * contains stale=true parameter (in other case we probably just failed\r
* due to invalid username / password)\r
*\r
* The method stores challenge values in $challenges static property\r
* - proxy auth credentials should be set in the request object\r
* - response should contain Proxy-Authenticate header with digest challenge\r
* - there is either no challenge stored for this proxy or new challenge\r
- * contains stale=true parameter (in other case we probably just failed \r
+ * contains stale=true parameter (in other case we probably just failed\r
* due to invalid username / password)\r
*\r
* The method stores challenge values in $challenges static property\r
* Extracts digest method challenge from (WWW|Proxy)-Authenticate header value\r
*\r
* There is a problem with implementation of RFC 2617: several of the parameters\r
- * here are defined as quoted-string and thus may contain backslash escaped\r
+ * are defined as quoted-string there and thus may contain backslash escaped\r
* double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as\r
* just value of quoted-string X without surrounding quotes, it doesn't speak\r
* about removing backslash escaping.\r
* - Squid allows (manually escaped) quotes there, but it is impossible to\r
* authorize with either escaped or unescaped quotes used in digest,\r
* probably it can't parse the response (?)\r
- * - Both IE and Firefox display realm value with backslashes in \r
+ * - Both IE and Firefox display realm value with backslashes in\r
* the password popup and apparently use the same value for digest\r
*\r
* HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in\r
- * quoted-string handling, unfortunately that means failure to authorize \r
+ * quoted-string handling, unfortunately that means failure to authorize\r
* sometimes\r
*\r
* @param string value of WWW-Authenticate or Proxy-Authenticate header\r
* @return mixed associative array with challenge parameters, false if\r
* no challenge is present in header value\r
- * @throws HTTP_Request2_Exception in case of unsupported challenge parameters\r
+ * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters\r
*/\r
protected function parseDigestChallenge($headerValue)\r
{\r
}\r
}\r
// we only support qop=auth\r
- if (!empty($paramsAry['qop']) && \r
+ if (!empty($paramsAry['qop']) &&\r
!in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))\r
) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Only 'auth' qop is currently supported in digest authentication, " .\r
"server requested '{$paramsAry['qop']}'"\r
);\r
}\r
// we only support algorithm=MD5\r
if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Only 'MD5' algorithm is currently supported in digest authentication, " .\r
"server requested '{$paramsAry['algorithm']}'"\r
);\r
}\r
\r
- return $paramsAry; \r
+ return $paramsAry;\r
}\r
\r
/**\r
* @param array challenge to update\r
* @param string value of [Proxy-]Authentication-Info header\r
* @todo validate server rspauth response\r
- */ \r
+ */\r
protected function updateChallenge(&$challenge, $headerValue)\r
{\r
$authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .\r
* @param array digest challenge parameters\r
* @return string value of [Proxy-]Authorization request header\r
* @link http://tools.ietf.org/html/rfc2617#section-3.2.2\r
- */ \r
+ */\r
protected function createDigestResponse($user, $password, $url, &$challenge)\r
{\r
- if (false !== ($q = strpos($url, '?')) && \r
+ if (false !== ($q = strpos($url, '?')) &&\r
$this->request->getConfig('digest_compat_ie')\r
) {\r
$url = substr($url, 0, $q);\r
'nonce="' . $challenge['nonce'] . '", ' .\r
'uri="' . $url . '", ' .\r
'response="' . $digest . '"' .\r
- (!empty($challenge['opaque'])? \r
+ (!empty($challenge['opaque'])?\r
', opaque="' . $challenge['opaque'] . '"':\r
'') .\r
(!empty($challenge['qop'])?\r
* @param array request headers\r
* @param string request host (needed for digest authentication)\r
* @param string request URL (needed for digest authentication)\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_NotImplementedException\r
*/\r
protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)\r
{\r
}\r
switch ($auth['scheme']) {\r
case HTTP_Request2::AUTH_BASIC:\r
- $headers['authorization'] = \r
+ $headers['authorization'] =\r
'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']);\r
break;\r
\r
foreach (array_keys(self::$challenges) as $key) {\r
if ($key == substr($fullUrl, 0, strlen($key))) {\r
$headers['authorization'] = $this->createDigestResponse(\r
- $auth['user'], $auth['password'], \r
+ $auth['user'], $auth['password'],\r
$requestUrl, self::$challenges[$key]\r
);\r
$this->serverChallenge =& self::$challenges[$key];\r
break;\r
\r
default:\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Unknown HTTP authentication scheme '{$auth['scheme']}'"\r
);\r
}\r
*\r
* @param array request headers\r
* @param string request URL (needed for digest authentication)\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_NotImplementedException\r
*/\r
protected function addProxyAuthorizationHeader(&$headers, $requestUrl)\r
{\r
break;\r
\r
default:\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Unknown HTTP authentication scheme '" .\r
$this->request->getConfig('proxy_auth_scheme') . "'"\r
);\r
) {\r
$headers['accept-encoding'] = 'gzip, deflate';\r
}\r
+ if (($jar = $this->request->getCookieJar())\r
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
+ ) {\r
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
+ }\r
\r
$this->addAuthorizationHeader($headers, $host, $requestUrl);\r
$this->addProxyAuthorizationHeader($headers, $requestUrl);\r
/**\r
* Sends the request body\r
*\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_MessageException\r
*/\r
protected function writeBody()\r
{\r
$str = $this->requestBody->read($bufferSize);\r
}\r
if (false === @fwrite($this->socket, $str, strlen($str))) {\r
- throw new HTTP_Request2_Exception('Error writing request');\r
+ throw new HTTP_Request2_MessageException('Error writing request');\r
}\r
// Provide the length of written string to the observer, request #7630\r
$this->request->setLastEvent('sentBodyPart', strlen($str));\r
- $position += strlen($str); \r
+ $position += strlen($str);\r
}\r
+ $this->request->setLastEvent('sentBody', $this->contentLength);\r
}\r
\r
/**\r
$bufferSize = $this->request->getConfig('buffer_size');\r
\r
do {\r
- $response = new HTTP_Request2_Response($this->readLine($bufferSize), true);\r
+ $response = new HTTP_Request2_Response(\r
+ $this->readLine($bufferSize), true, $this->request->getUrl()\r
+ );\r
do {\r
$headerLine = $this->readLine($bufferSize);\r
$response->parseHeaderLine($headerLine);\r
}\r
\r
/**\r
- * Reads until either the end of the socket or a newline, whichever comes first \r
+ * Reads until either the end of the socket or a newline, whichever comes first\r
*\r
- * Strips the trailing newline from the returned data, handles global \r
- * request timeout. Method idea borrowed from Net_Socket PEAR package. \r
+ * Strips the trailing newline from the returned data, handles global\r
+ * request timeout. Method idea borrowed from Net_Socket PEAR package.\r
*\r
* @param int buffer size to use for reading\r
* @return Available data up to the newline (not including newline)\r
- * @throws HTTP_Request2_Exception In case of timeout\r
+ * @throws HTTP_Request2_MessageException In case of timeout\r
*/\r
protected function readLine($bufferSize)\r
{\r
$line = '';\r
while (!feof($this->socket)) {\r
- if ($this->timeout) {\r
- stream_set_timeout($this->socket, max($this->timeout - time(), 1));\r
+ if ($this->deadline) {\r
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));\r
}\r
$line .= @fgets($this->socket, $bufferSize);\r
$info = stream_get_meta_data($this->socket);\r
- if ($info['timed_out'] || $this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $this->request->getConfig('timeout') . ' second(s)'\r
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {\r
+ $reason = $this->deadline\r
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'\r
+ : 'due to default_socket_timeout php.ini setting';\r
+ throw new HTTP_Request2_MessageException(\r
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
if (substr($line, -1) == "\n") {\r
*\r
* @param int Reads up to this number of bytes\r
* @return Data read from socket\r
- * @throws HTTP_Request2_Exception In case of timeout\r
+ * @throws HTTP_Request2_MessageException In case of timeout\r
*/\r
protected function fread($length)\r
{\r
- if ($this->timeout) {\r
- stream_set_timeout($this->socket, max($this->timeout - time(), 1));\r
+ if ($this->deadline) {\r
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));\r
}\r
$data = fread($this->socket, $length);\r
$info = stream_get_meta_data($this->socket);\r
- if ($info['timed_out'] || $this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $this->request->getConfig('timeout') . ' second(s)'\r
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {\r
+ $reason = $this->deadline\r
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'\r
+ : 'due to default_socket_timeout php.ini setting';\r
+ throw new HTTP_Request2_MessageException(\r
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
return $data;\r
*\r
* @param int buffer size to use for reading\r
* @return string\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_MessageException\r
*/\r
protected function readChunked($bufferSize)\r
{\r
if (0 == $this->chunkLength) {\r
$line = $this->readLine($bufferSize);\r
if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {\r
- throw new HTTP_Request2_Exception(\r
- "Cannot decode chunked response, invalid chunk length '{$line}'"\r
+ throw new HTTP_Request2_MessageException(\r
+ "Cannot decode chunked response, invalid chunk length '{$line}'",\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
);\r
} else {\r
$this->chunkLength = hexdec($matches[1]);\r
<?php\r
/**\r
- * Exception class for HTTP_Request2 package\r
+ * Exception classes for HTTP_Request2 package\r
*\r
* PHP version 5\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Exception.php 273003 2009-01-07 19:28:22Z avb $\r
+ * @version SVN: $Id: Exception.php 308629 2011-02-24 17:34:24Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
require_once 'PEAR/Exception.php';\r
\r
/**\r
- * Exception class for HTTP_Request2 package\r
- *\r
- * Such a class is required by the Exception RFC:\r
- * http://pear.php.net/pepr/pepr-proposal-show.php?id=132\r
+ * Base exception class for HTTP_Request2 package\r
*\r
* @category HTTP\r
* @package HTTP_Request2\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
+ * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132\r
*/\r
class HTTP_Request2_Exception extends PEAR_Exception\r
{\r
+ /** An invalid argument was passed to a method */\r
+ const INVALID_ARGUMENT = 1;\r
+ /** Some required value was not available */\r
+ const MISSING_VALUE = 2;\r
+ /** Request cannot be processed due to errors in PHP configuration */\r
+ const MISCONFIGURATION = 3;\r
+ /** Error reading the local file */\r
+ const READ_ERROR = 4;\r
+\r
+ /** Server returned a response that does not conform to HTTP protocol */\r
+ const MALFORMED_RESPONSE = 10;\r
+ /** Failure decoding Content-Encoding or Transfer-Encoding of response */\r
+ const DECODE_ERROR = 20;\r
+ /** Operation timed out */\r
+ const TIMEOUT = 30;\r
+ /** Number of redirects exceeded 'max_redirects' configuration parameter */\r
+ const TOO_MANY_REDIRECTS = 40;\r
+ /** Redirect to a protocol other than http(s):// */\r
+ const NON_HTTP_REDIRECT = 50;\r
+\r
+ /**\r
+ * Native error code\r
+ * @var int\r
+ */\r
+ private $_nativeCode;\r
+\r
+ /**\r
+ * Constructor, can set package error code and native error code\r
+ *\r
+ * @param string exception message\r
+ * @param int package error code, one of class constants\r
+ * @param int error code from underlying PHP extension\r
+ */\r
+ public function __construct($message = null, $code = null, $nativeCode = null)\r
+ {\r
+ parent::__construct($message, $code);\r
+ $this->_nativeCode = $nativeCode;\r
+ }\r
+\r
+ /**\r
+ * Returns error code produced by underlying PHP extension\r
+ *\r
+ * For Socket Adapter this may contain error number returned by\r
+ * stream_socket_client(), for Curl Adapter this will contain error number\r
+ * returned by curl_errno()\r
+ *\r
+ * @return integer\r
+ */\r
+ public function getNativeCode()\r
+ {\r
+ return $this->_nativeCode;\r
+ }\r
}\r
+\r
+/**\r
+ * Exception thrown in case of missing features\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @version Release: 2.0.0RC1\r
+ */\r
+class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception {}\r
+\r
+/**\r
+ * Exception that represents error in the program logic\r
+ *\r
+ * This exception usually implies a programmer's error, like passing invalid\r
+ * data to methods or trying to use PHP extensions that weren't installed or\r
+ * enabled. Usually exceptions of this kind will be thrown before request even\r
+ * starts.\r
+ *\r
+ * The exception will usually contain a package error code.\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @version Release: 2.0.0RC1\r
+ */\r
+class HTTP_Request2_LogicException extends HTTP_Request2_Exception {}\r
+\r
+/**\r
+ * Exception thrown when connection to a web or proxy server fails\r
+ *\r
+ * The exception will not contain a package error code, but will contain\r
+ * native error code, as returned by stream_socket_client() or curl_errno().\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @version Release: 2.0.0RC1\r
+ */\r
+class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception {}\r
+\r
+/**\r
+ * Exception thrown when sending or receiving HTTP message fails\r
+ *\r
+ * The exception may contain both package error code and native error code.\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @version Release: 2.0.0RC1\r
+ */\r
+class HTTP_Request2_MessageException extends HTTP_Request2_Exception {}\r
?>
\ No newline at end of file
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: MultipartBody.php 287306 2009-08-14 15:22:52Z avb $\r
+ * @version SVN: $Id: MultipartBody.php 308322 2011-02-14 13:58:03Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
* @link http://tools.ietf.org/html/rfc1867\r
*/\r
class HTTP_Request2_MultipartBody\r
while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) {\r
$oldLength = $length;\r
if ($this->_pos[0] < $paramCount) {\r
- $param = sprintf($this->_headerParam, $boundary, \r
+ $param = sprintf($this->_headerParam, $boundary,\r
$this->_params[$this->_pos[0]][0]) .\r
$this->_params[$this->_pos[0]][1] . "\r\n";\r
$ret .= substr($param, $this->_pos[1], $length);\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @author David Jean Louis <izi@php.net>\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Log.php 272593 2009-01-02 16:27:14Z avb $\r
+ * @version SVN: $Id: Log.php 308680 2011-02-25 17:40:17Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
/**\r
* Exception class for HTTP_Request2 package\r
- */ \r
+ */\r
require_once 'HTTP/Request2/Exception.php';\r
\r
/**\r
* A debug observer useful for debugging / testing.\r
*\r
- * This observer logs to a log target data corresponding to the various request \r
+ * This observer logs to a log target data corresponding to the various request\r
* and response events, it logs by default to php://output but can be configured\r
* to log to a file or via the PEAR Log package.\r
*\r
* @author David Jean Louis <izi@php.net>\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_Observer_Log implements SplObserver\r
public $events = array(\r
'connect',\r
'sentHeaders',\r
- 'sentBodyPart',\r
+ 'sentBody',\r
'receivedHeaders',\r
'receivedBody',\r
'disconnect',\r
}\r
if (is_resource($target) || $target instanceof Log) {\r
$this->target = $target;\r
- } elseif (false === ($this->target = @fopen($target, 'w'))) {\r
+ } elseif (false === ($this->target = @fopen($target, 'ab'))) {\r
throw new HTTP_Request2_Exception("Unable to open '{$target}'");\r
}\r
}\r
// update() {{{\r
\r
/**\r
- * Called when the request notify us of an event.\r
+ * Called when the request notifies us of an event.\r
*\r
* @param HTTP_Request2 $subject The HTTP_Request2 instance\r
*\r
$this->log('> ' . $header);\r
}\r
break;\r
- case 'sentBodyPart':\r
- $this->log('> ' . $event['data']);\r
+ case 'sentBody':\r
+ $this->log('> ' . $event['data'] . ' byte(s) sent');\r
break;\r
case 'receivedHeaders':\r
$this->log(sprintf('< HTTP/%s %s %s',\r
break;\r
}\r
}\r
- \r
+\r
// }}}\r
// log() {{{\r
\r
/**\r
- * Log the given message to the configured target.\r
+ * Logs the given message to the configured target.\r
*\r
* @param string $message Message to display\r
*\r
*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Response.php 287948 2009-09-01 17:12:18Z avb $\r
+ * @version SVN: $Id: Response.php 309921 2011-04-03 16:43:02Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
/**\r
* Exception class for HTTP_Request2 package\r
- */ \r
+ */\r
require_once 'HTTP/Request2/Exception.php';\r
\r
/**\r
* $headerLine = read_header_line();\r
* $response->parseHeaderLine($headerLine);\r
* } while ($headerLine != '');\r
- * \r
+ *\r
* while ($chunk = read_body()) {\r
* $response->appendBody($chunk);\r
* }\r
- * \r
+ *\r
* var_dump($response->getHeader(), $response->getCookies(), $response->getBody());\r
* </code>\r
*\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
* @link http://tools.ietf.org/html/rfc2616#section-6\r
*/\r
class HTTP_Request2_Response\r
*/\r
protected $reasonPhrase;\r
\r
+ /**\r
+ * Effective URL (may be different from original request URL in case of redirects)\r
+ * @var string\r
+ */\r
+ protected $effectiveUrl;\r
+\r
/**\r
* Associative array of response headers\r
* @var array\r
305 => 'Use Proxy',\r
307 => 'Temporary Redirect',\r
\r
- // 4xx: Client Error - The request contains bad syntax or cannot be \r
+ // 4xx: Client Error - The request contains bad syntax or cannot be\r
// fulfilled\r
400 => 'Bad Request',\r
401 => 'Unauthorized',\r
/**\r
* Constructor, parses the response status line\r
*\r
- * @param string Response status line (e.g. "HTTP/1.1 200 OK")\r
- * @param bool Whether body is still encoded by Content-Encoding\r
- * @throws HTTP_Request2_Exception if status line is invalid according to spec\r
+ * @param string Response status line (e.g. "HTTP/1.1 200 OK")\r
+ * @param bool Whether body is still encoded by Content-Encoding\r
+ * @param string Effective URL of the response\r
+ * @throws HTTP_Request2_MessageException if status line is invalid according to spec\r
*/\r
- public function __construct($statusLine, $bodyEncoded = true)\r
+ public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null)\r
{\r
if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) {\r
- throw new HTTP_Request2_Exception("Malformed response: {$statusLine}");\r
+ throw new HTTP_Request2_MessageException(\r
+ "Malformed response: {$statusLine}",\r
+ HTTP_Request2_Exception::MALFORMED_RESPONSE\r
+ );\r
}\r
$this->version = $m[1];\r
$this->code = intval($m[2]);\r
} elseif (!empty(self::$phrases[$this->code])) {\r
$this->reasonPhrase = self::$phrases[$this->code];\r
}\r
- $this->bodyEncoded = (bool)$bodyEncoded;\r
+ $this->bodyEncoded = (bool)$bodyEncoded;\r
+ $this->effectiveUrl = (string)$effectiveUrl;\r
}\r
\r
/**\r
* Parses the line from HTTP response filling $headers array\r
*\r
- * The method should be called after reading the line from socket or receiving \r
+ * The method should be called after reading the line from socket or receiving\r
* it into cURL callback. Passing an empty string here indicates the end of\r
* response headers and triggers additional processing, so be sure to pass an\r
* empty string in the end.\r
}\r
$this->lastHeader = $name;\r
\r
- // string \r
+ // continuation of a previous header\r
} elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) {\r
if (!is_array($this->headers[$this->lastHeader])) {\r
$this->headers[$this->lastHeader] .= ' ' . trim($m[1]);\r
$this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]);\r
}\r
}\r
- } \r
+ }\r
\r
/**\r
* Parses a Set-Cookie header to fill $cookies array\r
*\r
* @param string value of Set-Cookie header\r
- * @link http://cgi.netscape.com/newsref/std/cookie_spec.html\r
+ * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html\r
*/\r
protected function parseCookie($cookieString)\r
{\r
$this->body .= $bodyChunk;\r
}\r
\r
+ /**\r
+ * Returns the effective URL of the response\r
+ *\r
+ * This may be different from the request URL if redirects were followed.\r
+ *\r
+ * @return string\r
+ * @link http://pear.php.net/bugs/bug.php?id=18412\r
+ */\r
+ public function getEffectiveUrl()\r
+ {\r
+ return $this->effectiveUrl;\r
+ }\r
+\r
/**\r
* Returns the status code\r
- * @return integer \r
+ * @return integer\r
*/\r
public function getStatus()\r
{\r
return $this->reasonPhrase;\r
}\r
\r
+ /**\r
+ * Whether response is a redirect that can be automatically handled by HTTP_Request2\r
+ * @return bool\r
+ */\r
+ public function isRedirect()\r
+ {\r
+ return in_array($this->code, array(300, 301, 302, 303, 307))\r
+ && isset($this->headers['location']);\r
+ }\r
+\r
/**\r
* Returns either the named header or all response headers\r
*\r
*/\r
public function getBody()\r
{\r
- if (!$this->bodyEncoded ||\r
+ if (0 == strlen($this->body) || !$this->bodyEncoded ||\r
!in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate'))\r
) {\r
return $this->body;\r
* Get the HTTP version of the response\r
*\r
* @return string\r
- */ \r
+ */\r
public function getVersion()\r
{\r
return $this->version;\r
*\r
* @param string gzip-encoded data\r
* @return string decoded data\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
+ * @throws HTTP_Request2_MessageException\r
* @link http://tools.ietf.org/html/rfc1952\r
*/\r
public static function decodeGzip($data)\r
return $data;\r
}\r
if (!function_exists('gzinflate')) {\r
- throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Unable to decode body: gzip extension not available',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
}\r
$method = ord(substr($data, 2, 1));\r
if (8 != $method) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: unknown compression method');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: unknown compression method',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$flags = ord(substr($data, 3, 1));\r
if ($flags & 224) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: reserved bits are set');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: reserved bits are set',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
\r
// header is 10 bytes minimum. may be longer, though.\r
// extra fields, need to skip 'em\r
if ($flags & 4) {\r
if ($length - $headerLength - 2 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$extraLength = unpack('v', substr($data, 10, 2));\r
if ($length - $headerLength - 2 - $extraLength[1] < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$headerLength += $extraLength[1] + 2;\r
}\r
// file name, need to skip that\r
if ($flags & 8) {\r
if ($length - $headerLength - 1 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$filenameLength = strpos(substr($data, $headerLength), chr(0));\r
if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$headerLength += $filenameLength + 1;\r
}\r
// comment, need to skip that also\r
if ($flags & 16) {\r
if ($length - $headerLength - 1 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$commentLength = strpos(substr($data, $headerLength), chr(0));\r
if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$headerLength += $commentLength + 1;\r
}\r
// have a CRC for header. let's check\r
if ($flags & 2) {\r
if ($length - $headerLength - 2 < 8) {\r
- throw new HTTP_Request2_Exception('Error parsing gzip header: data too short');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Error parsing gzip header: data too short',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$crcReal = 0xffff & crc32(substr($data, 0, $headerLength));\r
$crcStored = unpack('v', substr($data, $headerLength, 2));\r
if ($crcReal != $crcStored[1]) {\r
- throw new HTTP_Request2_Exception('Header CRC check failed');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Header CRC check failed',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
$headerLength += 2;\r
}\r
// don't pass $dataSize to gzinflate, see bugs #13135, #14370\r
$unpacked = gzinflate(substr($data, $headerLength, -8));\r
if (false === $unpacked) {\r
- throw new HTTP_Request2_Exception('gzinflate() call failed');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'gzinflate() call failed',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
} elseif ($dataSize != strlen($unpacked)) {\r
- throw new HTTP_Request2_Exception('Data size check failed');\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Data size check failed',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
} elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {\r
- throw new HTTP_Request2_Exception('Data CRC check failed');\r
+ throw new HTTP_Request2_Exception(\r
+ 'Data CRC check failed',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
+ );\r
}\r
return $unpacked;\r
}\r
*\r
* @param string deflate-encoded data\r
* @return string decoded data\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
public static function decodeDeflate($data)\r
{\r
if (!function_exists('gzuncompress')) {\r
- throw new HTTP_Request2_Exception('Unable to decode body: gzip extension not available');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Unable to decode body: gzip extension not available',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
}\r
// RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,\r
// while many applications send raw deflate stream from RFC 1951.\r