* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
/**\r
* A class representing an URL as per RFC 3986.\r
*/\r
-require_once 'Net/URL2.php';\r
+if (!class_exists('Net_URL2', true)) {\r
+ require_once 'Net/URL2.php';\r
+}\r
\r
/**\r
* Exception class for HTTP_Request2 package\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://tools.ietf.org/html/rfc2616#section-5\r
*/\r
$this->setMethod($method);\r
}\r
$this->setHeader(\r
- 'user-agent', 'HTTP_Request2/2.2.1 ' .\r
+ 'user-agent', 'HTTP_Request2/2.3.0 ' .\r
'(http://pear.php.net/package/http_request2) PHP/' . phpversion()\r
);\r
}\r
* encoded by Content-Encoding</li>\r
* <li>'receivedBody' - after receiving the complete response\r
* body, data is HTTP_Request2_Response object</li>\r
+ * <li>'warning' - a problem arose during the request\r
+ * that is not severe enough to throw\r
+ * an Exception, data is the warning\r
+ * message (string). Currently dispatched if\r
+ * response body was received incompletely.</li>\r
* </ul>\r
* Different adapters may not send all the event types. Mock adapter does\r
* not send any events to the observers.\r
}\r
// (deprecated) mime_content_type function available\r
if (empty($info) && function_exists('mime_content_type')) {\r
- return mime_content_type($filename);\r
+ $info = mime_content_type($filename);\r
}\r
return empty($info)? 'application/octet-stream': $info;\r
}\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
abstract class HTTP_Request2_Adapter\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter\r
*/\r
protected $eventReceivedHeaders = false;\r
\r
+ /**\r
+ * Whether 'sentBoody' event was sent to observers\r
+ * @var boolean\r
+ */\r
+ protected $eventSentBody = false;\r
+\r
/**\r
* Position within request body\r
* @var integer\r
$this->position = 0;\r
$this->eventSentHeaders = false;\r
$this->eventReceivedHeaders = false;\r
+ $this->eventSentBody = false;\r
\r
try {\r
if (false === curl_exec($ch = $this->createCurlHandle())) {\r
}\r
if (isset($ch)) {\r
$this->lastInfo = curl_getinfo($ch);\r
+ if (CURLE_OK !== curl_errno($ch)) {\r
+ $this->request->setLastEvent('warning', curl_error($ch));\r
+ }\r
curl_close($ch);\r
}\r
\r
}\r
\r
if ($jar = $request->getCookieJar()) {\r
- $jar->addCookiesFromResponse($response, $request->getUrl());\r
+ $jar->addCookiesFromResponse($response);\r
}\r
\r
if (0 < $this->lastInfo['size_download']) {\r
protected function workaroundPhpBug47204($ch, &$headers)\r
{\r
// no redirects, no digest auth -> probably no rewind needed\r
+ // also apply workaround only for POSTs, othrerwise we get\r
+ // https://pear.php.net/bugs/bug.php?id=20440 for PUTs\r
if (!$this->request->getConfig('follow_redirects')\r
&& (!($auth = $this->request->getAuth())\r
|| HTTP_Request2::AUTH_DIGEST != $auth['scheme'])\r
+ || HTTP_Request2::METHOD_POST !== $this->request->getMethod()\r
) {\r
curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));\r
\r
*/\r
protected function callbackWriteHeader($ch, $string)\r
{\r
- // we may receive a second set of headers if doing e.g. digest auth\r
- if ($this->eventReceivedHeaders || !$this->eventSentHeaders) {\r
- // don't bother with 100-Continue responses (bug #15785)\r
- if (!$this->eventSentHeaders\r
- || $this->response->getStatus() >= 200\r
- ) {\r
- $this->request->setLastEvent(\r
- 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
- );\r
- }\r
+ if (!$this->eventSentHeaders\r
+ // we may receive a second set of headers if doing e.g. digest auth\r
+ // but don't bother with 100-Continue responses (bug #15785)\r
+ || $this->eventReceivedHeaders && $this->response->getStatus() >= 200\r
+ ) {\r
+ $this->request->setLastEvent(\r
+ 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)\r
+ );\r
+ }\r
+ if (!$this->eventSentBody) {\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 body wasn't read by the 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
+ if ($upload > 0) {\r
$this->request->setLastEvent('sentBody', $upload);\r
}\r
- $this->eventSentHeaders = true;\r
- // we'll need a new response object\r
- if ($this->eventReceivedHeaders) {\r
- $this->eventReceivedHeaders = false;\r
- $this->response = null;\r
- }\r
}\r
- if (empty($this->response)) {\r
- $this->response = new HTTP_Request2_Response(\r
+ $this->eventSentHeaders = true;\r
+ $this->eventSentBody = true;\r
+\r
+ if ($this->eventReceivedHeaders || empty($this->response)) {\r
+ $this->eventReceivedHeaders = false;\r
+ $this->response = new HTTP_Request2_Response(\r
$string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)\r
);\r
+\r
} else {\r
$this->response->parseHeaderLine($string);\r
if ('' == trim($string)) {\r
}\r
\r
if ($jar = $this->request->getCookieJar()) {\r
- $jar->addCookiesFromResponse($this->response, $this->request->getUrl());\r
+ $jar->addCookiesFromResponse($this->response);\r
if (!$redirectUrl->isAbsolute()) {\r
$redirectUrl = $this->request->getUrl()->resolve($redirectUrl);\r
}\r
}\r
}\r
$this->eventReceivedHeaders = true;\r
+ $this->eventSentBody = false;\r
}\r
}\r
return strlen($string);\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter\r
\r
\r
if ($jar = $request->getCookieJar()) {\r
- $jar->addCookiesFromResponse($response, $request->getUrl());\r
+ $jar->addCookiesFromResponse($response);\r
}\r
\r
if (!$this->canKeepAlive($keepAlive, $response)) {\r
foreach ($this->request->getConfig() as $name => $value) {\r
if ('ssl_' == substr($name, 0, 4) && null !== $value) {\r
if ('ssl_verify_host' == $name) {\r
- if ($value) {\r
- $options['ssl']['CN_match'] = $reqHost;\r
+ if (version_compare(phpversion(), '5.6', '<')) {\r
+ if ($value) {\r
+ $options['ssl']['CN_match'] = $reqHost;\r
+ }\r
+\r
+ } else {\r
+ $options['ssl']['verify_peer_name'] = $value;\r
+ $options['ssl']['peer_name'] = $reqHost;\r
}\r
+\r
} else {\r
$options['ssl'][substr($name, 4)] = $value;\r
}\r
\r
// Changing SSL context options after connection is established does *not*\r
// work, we need a new connection if options change\r
- $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://')\r
+ $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'tls://')\r
. $host . ':' . $port;\r
$socketKey = $remote . (\r
($secure && $httpProxy || $socksProxy)\r
$conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}";\r
} else {\r
$this->socket->enableCrypto();\r
- $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";\r
+ $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}";\r
}\r
\r
} elseif ($secure && $httpProxy && !$tunnel) {\r
$this->establishTunnel();\r
- $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";\r
+ $conninfo = "tls://{$reqHost}:{$reqPort} via {$remote}";\r
\r
} else {\r
$this->socket = new HTTP_Request2_SocketWrapper(\r
$chunked = 'chunked' == $response->getHeader('transfer-encoding');\r
$length = $response->getHeader('content-length');\r
$hasBody = false;\r
- if ($chunked || null === $length || 0 < intval($length)) {\r
- // RFC 2616, section 4.4:\r
- // 3. ... If a message is received with both a\r
- // Transfer-Encoding header field and a Content-Length header field,\r
- // the latter MUST be ignored.\r
- $toRead = ($chunked || null === $length)? null: $length;\r
- $this->chunkLength = 0;\r
+ // RFC 2616, section 4.4:\r
+ // 3. ... If a message is received with both a\r
+ // Transfer-Encoding header field and a Content-Length header field,\r
+ // the latter MUST be ignored.\r
+ $toRead = ($chunked || null === $length)? null: $length;\r
+ $this->chunkLength = 0;\r
\r
+ if ($chunked || null === $length || 0 < intval($length)) {\r
while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) {\r
if ($chunked) {\r
$data = $this->readChunked($bufferSize);\r
}\r
}\r
}\r
+ if (0 !== $this->chunkLength || null !== $toRead && $toRead > 0) {\r
+ $this->request->setLastEvent(\r
+ 'warning', 'transfer closed with outstanding read data remaining'\r
+ );\r
+ }\r
\r
if ($hasBody) {\r
$this->request->setLastEvent('receivedBody', $response);\r
// at start of the next chunk?\r
if (0 == $this->chunkLength) {\r
$line = $this->socket->readLine($bufferSize);\r
- if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {\r
+ if ('' === $line && $this->socket->eof()) {\r
+ $this->chunkLength = -1; // indicate missing chunk\r
+ return '';\r
+\r
+ } elseif (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {\r
throw new HTTP_Request2_MessageException(\r
"Cannot decode chunked response, invalid chunk length '{$line}'",\r
HTTP_Request2_Exception::DECODE_ERROR\r
);\r
+\r
} else {\r
$this->chunkLength = hexdec($matches[1]);\r
// Chunk with zero length indicates the end\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
*/\r
protected $useList = true;\r
\r
+ /**\r
+ * Whether an attempt to store an invalid cookie should be ignored, rather than cause an Exception\r
+ * @var bool\r
+ */\r
+ protected $ignoreInvalid = false;\r
+\r
/**\r
* Array with Public Suffix List data\r
* @var array\r
* see {@link serializeSessionCookies()}\r
* @param bool $usePublicSuffixList Controls using Public Suffix List,\r
* see {@link usePublicSuffixList()}\r
+ * @param bool $ignoreInvalidCookies Whether invalid cookies should be ignored,\r
+ * see {@link ignoreInvalidCookies()}\r
*/\r
public function __construct(\r
- $serializeSessionCookies = false, $usePublicSuffixList = true\r
+ $serializeSessionCookies = false, $usePublicSuffixList = true,\r
+ $ignoreInvalidCookies = false\r
) {\r
$this->serializeSessionCookies($serializeSessionCookies);\r
$this->usePublicSuffixList($usePublicSuffixList);\r
+ $this->ignoreInvalidCookies($ignoreInvalidCookies);\r
}\r
\r
/**\r
* {@link HTTP_Request2_Response::getCookies()}\r
* @param Net_URL2 $setter URL of the document that sent Set-Cookie header\r
*\r
- * @throws HTTP_Request2_Exception\r
+ * @return bool whether the cookie was successfully stored\r
+ * @throws HTTP_Request2_Exception\r
*/\r
public function store(array $cookie, Net_URL2 $setter = null)\r
{\r
- $cookie = $this->checkAndUpdateFields($cookie, $setter);\r
+ try {\r
+ $cookie = $this->checkAndUpdateFields($cookie, $setter);\r
+ } catch (HTTP_Request2_Exception $e) {\r
+ if ($this->ignoreInvalid) {\r
+ return false;\r
+ } else {\r
+ throw $e;\r
+ }\r
+ }\r
\r
if (strlen($cookie['value'])\r
&& (is_null($cookie['expires']) || $cookie['expires'] > $this->now())\r
} elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) {\r
unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]);\r
}\r
+\r
+ return true;\r
}\r
\r
/**\r
*\r
* @param HTTP_Request2_Response $response HTTP response message\r
* @param Net_URL2 $setter original request URL, needed for\r
- * setting default domain/path\r
+ * setting default domain/path. If not given,\r
+ * effective URL from response will be used.\r
+ *\r
+ * @return bool whether all cookies were successfully stored\r
+ * @throws HTTP_Request2_LogicException\r
*/\r
- public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter)\r
+ public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter = null)\r
{\r
+ if (null === $setter) {\r
+ if (!($effectiveUrl = $response->getEffectiveUrl())) {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Response URL required for adding cookies from response',\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
+ }\r
+ $setter = new Net_URL2($effectiveUrl);\r
+ }\r
+\r
+ $success = true;\r
foreach ($response->getCookies() as $cookie) {\r
- $this->store($cookie, $setter);\r
+ $success = $this->store($cookie, $setter) && $success;\r
}\r
+ return $success;\r
}\r
\r
/**\r
$this->serializeSession = (bool)$serialize;\r
}\r
\r
+ /**\r
+ * Sets whether invalid cookies should be silently ignored or cause an Exception\r
+ *\r
+ * @param boolean $ignore ignore?\r
+ * @link http://pear.php.net/bugs/bug.php?id=19937\r
+ * @link http://pear.php.net/bugs/bug.php?id=20401\r
+ */\r
+ public function ignoreInvalidCookies($ignore)\r
+ {\r
+ $this->ignoreInvalid = (bool)$ignore;\r
+ }\r
+\r
/**\r
* Sets whether Public Suffix List should be used for restricting cookie-setting\r
*\r
return serialize(array(\r
'cookies' => $cookies,\r
'serializeSession' => $this->serializeSession,\r
- 'useList' => $this->useList\r
+ 'useList' => $this->useList,\r
+ 'ignoreInvalid' => $this->ignoreInvalid\r
));\r
}\r
\r
$now = $this->now();\r
$this->serializeSessionCookies($data['serializeSession']);\r
$this->usePublicSuffixList($data['useList']);\r
+ if (array_key_exists('ignoreInvalid', $data)) {\r
+ $this->ignoreInvalidCookies($data['ignoreInvalid']);\r
+ }\r
foreach ($data['cookies'] as $cookie) {\r
if (!empty($cookie['expires']) && $cookie['expires'] <= $now) {\r
continue;\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_LogicException extends HTTP_Request2_Exception\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_MessageException extends HTTP_Request2_Exception\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://tools.ietf.org/html/rfc1867\r
*/\r
* @package HTTP_Request2\r
* @author David Jean Louis <izi@php.net>\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @author David Jean Louis <izi@php.net>\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
class HTTP_Request2_Observer_Log implements SplObserver\r
--- /dev/null
+<?php\r
+/**\r
+ * An observer that saves response body to stream, possibly uncompressing it\r
+ *\r
+ * PHP version 5\r
+ *\r
+ * LICENSE\r
+ *\r
+ * This source file is subject to BSD 3-Clause License that is bundled\r
+ * with this package in the file LICENSE and available at the URL\r
+ * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @author Delian Krustev <krustev@krustev.net>\r
+ * @author Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
+ * @link http://pear.php.net/package/HTTP_Request2\r
+ */\r
+\r
+require_once 'HTTP/Request2/Response.php';\r
+\r
+/**\r
+ * An observer that saves response body to stream, possibly uncompressing it\r
+ *\r
+ * This Observer is written in compliment to pear's HTTP_Request2 in order to\r
+ * avoid reading the whole response body in memory. Instead it writes the body\r
+ * to a stream. If the body is transferred with content-encoding set to\r
+ * "deflate" or "gzip" it is decoded on the fly.\r
+ *\r
+ * The constructor accepts an already opened (for write) stream (file_descriptor).\r
+ * If the response is deflate/gzip encoded a "zlib.inflate" filter is applied\r
+ * to the stream. When the body has been read from the request and written to\r
+ * the stream ("receivedBody" event) the filter is removed from the stream.\r
+ *\r
+ * The "zlib.inflate" filter works fine with pure "deflate" encoding. It does\r
+ * not understand the "deflate+zlib" and "gzip" headers though, so they have to\r
+ * be removed prior to being passed to the stream. This is done in the "update"\r
+ * method.\r
+ *\r
+ * It is also possible to limit the size of written extracted bytes by passing\r
+ * "max_bytes" to the constructor. This is important because e.g. 1GB of\r
+ * zeroes take about a MB when compressed.\r
+ *\r
+ * Exceptions are being thrown if data could not be written to the stream or\r
+ * the written bytes have already exceeded the requested maximum. If the "gzip"\r
+ * header is malformed or could not be parsed an exception will be thrown too.\r
+ *\r
+ * Example usage follows:\r
+ *\r
+ * <code>\r
+ * require_once 'HTTP/Request2.php';\r
+ * require_once 'HTTP/Request2/Observer/UncompressingDownload.php';\r
+ *\r
+ * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html';\r
+ * #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on';\r
+ * $inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on&zlib=on';\r
+ * #$outPath = "/dev/null";\r
+ * $outPath = "delme";\r
+ *\r
+ * $stream = fopen($outPath, 'wb');\r
+ * if (!$stream) {\r
+ * throw new Exception('fopen failed');\r
+ * }\r
+ *\r
+ * $request = new HTTP_Request2(\r
+ * $inPath,\r
+ * HTTP_Request2::METHOD_GET,\r
+ * array(\r
+ * 'store_body' => false,\r
+ * 'connect_timeout' => 5,\r
+ * 'timeout' => 10,\r
+ * 'ssl_verify_peer' => true,\r
+ * 'ssl_verify_host' => true,\r
+ * 'ssl_cafile' => null,\r
+ * 'ssl_capath' => '/etc/ssl/certs',\r
+ * 'max_redirects' => 10,\r
+ * 'follow_redirects' => true,\r
+ * 'strict_redirects' => false\r
+ * )\r
+ * );\r
+ *\r
+ * $observer = new HTTP_Request2_Observer_UncompressingDownload($stream, 9999999);\r
+ * $request->attach($observer);\r
+ *\r
+ * $response = $request->send();\r
+ *\r
+ * fclose($stream);\r
+ * echo "OK\n";\r
+ * </code>\r
+ *\r
+ * @category HTTP\r
+ * @package HTTP_Request2\r
+ * @author Delian Krustev <krustev@krustev.net>\r
+ * @author Alexey Borzov <avb@php.net>\r
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
+ * @version Release: 2.3.0\r
+ * @link http://pear.php.net/package/HTTP_Request2\r
+ */\r
+class HTTP_Request2_Observer_UncompressingDownload implements SplObserver\r
+{\r
+ /**\r
+ * The stream to write response body to\r
+ * @var resource\r
+ */\r
+ private $_stream;\r
+\r
+ /**\r
+ * zlib.inflate filter possibly added to stream\r
+ * @var resource\r
+ */\r
+ private $_streamFilter;\r
+\r
+ /**\r
+ * The value of response's Content-Encoding header\r
+ * @var string\r
+ */\r
+ private $_encoding;\r
+\r
+ /**\r
+ * Whether the observer is still waiting for gzip/deflate header\r
+ * @var bool\r
+ */\r
+ private $_processingHeader = true;\r
+\r
+ /**\r
+ * Starting position in the stream observer writes to\r
+ * @var int\r
+ */\r
+ private $_startPosition = 0;\r
+\r
+ /**\r
+ * Maximum bytes to write\r
+ * @var int|null\r
+ */\r
+ private $_maxDownloadSize;\r
+\r
+ /**\r
+ * Whether response being received is a redirect\r
+ * @var bool\r
+ */\r
+ private $_redirect = false;\r
+\r
+ /**\r
+ * Accumulated body chunks that may contain (gzip) header\r
+ * @var string\r
+ */\r
+ private $_possibleHeader = '';\r
+\r
+ /**\r
+ * Class constructor\r
+ *\r
+ * Note that there might be problems with max_bytes and files bigger\r
+ * than 2 GB on 32bit platforms\r
+ *\r
+ * @param resource $stream a stream (or file descriptor) opened for writing.\r
+ * @param int $maxDownloadSize maximum bytes to write\r
+ */\r
+ public function __construct($stream, $maxDownloadSize = null)\r
+ {\r
+ $this->_stream = $stream;\r
+ if ($maxDownloadSize) {\r
+ $this->_maxDownloadSize = $maxDownloadSize;\r
+ $this->_startPosition = ftell($this->_stream);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Called when the request notifies us of an event.\r
+ *\r
+ * @param SplSubject $request The HTTP_Request2 instance\r
+ *\r
+ * @return void\r
+ * @throws HTTP_Request2_MessageException\r
+ */\r
+ public function update(SplSubject $request)\r
+ {\r
+ /* @var $request HTTP_Request2 */\r
+ $event = $request->getLastEvent();\r
+ $encoded = false;\r
+\r
+ /* @var $event['data'] HTTP_Request2_Response */\r
+ switch ($event['name']) {\r
+ case 'receivedHeaders':\r
+ $this->_processingHeader = true;\r
+ $this->_redirect = $event['data']->isRedirect();\r
+ $this->_encoding = strtolower($event['data']->getHeader('content-encoding'));\r
+ $this->_possibleHeader = '';\r
+ break;\r
+\r
+ case 'receivedEncodedBodyPart':\r
+ if (!$this->_streamFilter\r
+ && ($this->_encoding === 'deflate' || $this->_encoding === 'gzip')\r
+ ) {\r
+ $this->_streamFilter = stream_filter_append(\r
+ $this->_stream, 'zlib.inflate', STREAM_FILTER_WRITE\r
+ );\r
+ }\r
+ $encoded = true;\r
+ // fall-through is intentional\r
+\r
+ case 'receivedBodyPart':\r
+ if ($this->_redirect) {\r
+ break;\r
+ }\r
+\r
+ if (!$encoded || !$this->_processingHeader) {\r
+ $bytes = fwrite($this->_stream, $event['data']);\r
+\r
+ } else {\r
+ $offset = 0;\r
+ $this->_possibleHeader .= $event['data'];\r
+ if ('deflate' === $this->_encoding) {\r
+ if (2 > strlen($this->_possibleHeader)) {\r
+ break;\r
+ }\r
+ $header = unpack('n', substr($this->_possibleHeader, 0, 2));\r
+ if (0 == $header[1] % 31) {\r
+ $offset = 2;\r
+ }\r
+\r
+ } elseif ('gzip' === $this->_encoding) {\r
+ if (10 > strlen($this->_possibleHeader)) {\r
+ break;\r
+ }\r
+ try {\r
+ $offset = HTTP_Request2_Response::parseGzipHeader($this->_possibleHeader, false);\r
+\r
+ } catch (HTTP_Request2_MessageException $e) {\r
+ // need more data?\r
+ if (false !== strpos($e->getMessage(), 'data too short')) {\r
+ break;\r
+ }\r
+ throw $e;\r
+ }\r
+ }\r
+\r
+ $this->_processingHeader = false;\r
+ $bytes = fwrite($this->_stream, substr($this->_possibleHeader, $offset));\r
+ }\r
+\r
+ if (false === $bytes) {\r
+ throw new HTTP_Request2_MessageException('fwrite failed.');\r
+ }\r
+\r
+ if ($this->_maxDownloadSize\r
+ && ftell($this->_stream) - $this->_startPosition > $this->_maxDownloadSize\r
+ ) {\r
+ throw new HTTP_Request2_MessageException(sprintf(\r
+ 'Body length limit (%d bytes) reached',\r
+ $this->_maxDownloadSize\r
+ ));\r
+ }\r
+ break;\r
+\r
+ case 'receivedBody':\r
+ if ($this->_streamFilter) {\r
+ stream_filter_remove($this->_streamFilter);\r
+ $this->_streamFilter = null;\r
+ }\r
+ break;\r
+ }\r
+ }\r
+}\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://tools.ietf.org/html/rfc2616#section-6\r
*/\r
}\r
\r
/**\r
- * Decodes the message-body encoded by gzip\r
+ * Checks whether data starts with GZIP format identification bytes from RFC 1952\r
*\r
- * The real decoding work is done by gzinflate() built-in function, this\r
- * method only parses the header and checks data for compliance with\r
- * RFC 1952\r
+ * @param string $data gzip-encoded (presumably) data\r
*\r
- * @param string $data gzip-encoded data\r
+ * @return bool\r
+ */\r
+ public static function hasGzipIdentification($data)\r
+ {\r
+ return 0 === strcmp(substr($data, 0, 2), "\x1f\x8b");\r
+ }\r
+\r
+ /**\r
+ * Tries to parse GZIP format header in the given string\r
*\r
- * @return string decoded data\r
- * @throws HTTP_Request2_LogicException\r
- * @throws HTTP_Request2_MessageException\r
- * @link http://tools.ietf.org/html/rfc1952\r
+ * If the header conforms to RFC 1952, its length is returned. If any\r
+ * sanity check fails, HTTP_Request2_MessageException is thrown.\r
+ *\r
+ * Note: This function might be usable outside of HTTP_Request2 so it might\r
+ * be good idea to be moved to some common package. (Delian Krustev)\r
+ *\r
+ * @param string $data Either the complete response body or\r
+ * the leading part of it\r
+ * @param boolean $dataComplete Whether $data contains complete response body\r
+ *\r
+ * @return int gzip header length in bytes\r
+ * @throws HTTP_Request2_MessageException\r
+ * @link http://tools.ietf.org/html/rfc1952\r
*/\r
- public static function decodeGzip($data)\r
+ public static function parseGzipHeader($data, $dataComplete = false)\r
{\r
- $length = strlen($data);\r
- // If it doesn't look like gzip-encoded data, don't bother\r
- if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {\r
- return $data;\r
- }\r
- if (!function_exists('gzinflate')) {\r
- throw new HTTP_Request2_LogicException(\r
- 'Unable to decode body: gzip extension not available',\r
- HTTP_Request2_Exception::MISCONFIGURATION\r
+ // if data is complete, trailing 8 bytes should be present for size and crc32\r
+ $length = strlen($data) - ($dataComplete ? 8 : 0);\r
+\r
+ if ($length < 10 || !self::hasGzipIdentification($data)) {\r
+ throw new HTTP_Request2_MessageException(\r
+ 'The data does not seem to contain a valid gzip header',\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
);\r
}\r
+\r
$method = ord(substr($data, 2, 1));\r
if (8 != $method) {\r
throw new HTTP_Request2_MessageException(\r
$headerLength = 10;\r
// extra fields, need to skip 'em\r
if ($flags & 4) {\r
- if ($length - $headerLength - 2 < 8) {\r
+ if ($length - $headerLength - 2 < 0) {\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
+ if ($length - $headerLength - 2 - $extraLength[1] < 0) {\r
throw new HTTP_Request2_MessageException(\r
'Error parsing gzip header: data too short',\r
HTTP_Request2_Exception::DECODE_ERROR\r
}\r
// file name, need to skip that\r
if ($flags & 8) {\r
- if ($length - $headerLength - 1 < 8) {\r
+ if ($length - $headerLength - 1 < 0) {\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
+ if (false === $filenameLength\r
+ || $length - $headerLength - $filenameLength - 1 < 0\r
+ ) {\r
throw new HTTP_Request2_MessageException(\r
'Error parsing gzip header: data too short',\r
HTTP_Request2_Exception::DECODE_ERROR\r
}\r
// comment, need to skip that also\r
if ($flags & 16) {\r
- if ($length - $headerLength - 1 < 8) {\r
+ if ($length - $headerLength - 1 < 0) {\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
+ if (false === $commentLength\r
+ || $length - $headerLength - $commentLength - 1 < 0\r
+ ) {\r
throw new HTTP_Request2_MessageException(\r
'Error parsing gzip header: data too short',\r
HTTP_Request2_Exception::DECODE_ERROR\r
}\r
// have a CRC for header. let's check\r
if ($flags & 2) {\r
- if ($length - $headerLength - 2 < 8) {\r
+ if ($length - $headerLength - 2 < 0) {\r
throw new HTTP_Request2_MessageException(\r
'Error parsing gzip header: data too short',\r
HTTP_Request2_Exception::DECODE_ERROR\r
}\r
$headerLength += 2;\r
}\r
+ return $headerLength;\r
+ }\r
+\r
+ /**\r
+ * Decodes the message-body encoded by gzip\r
+ *\r
+ * The real decoding work is done by gzinflate() built-in function, this\r
+ * method only parses the header and checks data for compliance with\r
+ * RFC 1952\r
+ *\r
+ * @param string $data gzip-encoded data\r
+ *\r
+ * @return string decoded data\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
+ {\r
+ // If it doesn't look like gzip-encoded data, don't bother\r
+ if (!self::hasGzipIdentification($data)) {\r
+ return $data;\r
+ }\r
+ if (!function_exists('gzinflate')) {\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Unable to decode body: gzip extension not available',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
+ );\r
+ }\r
+\r
// unpacked data CRC and size at the end of encoded data\r
$tmp = unpack('V2', substr($data, -8));\r
$dataCrc = $tmp[1];\r
$dataSize = $tmp[2];\r
\r
- // finally, call the gzinflate() function\r
+ $headerLength = self::parseGzipHeader($data, true);\r
+\r
// don't pass $dataSize to gzinflate, see bugs #13135, #14370\r
$unpacked = gzinflate(substr($data, $headerLength, -8));\r
if (false === $unpacked) {\r
HTTP_Request2_Exception::DECODE_ERROR\r
);\r
} elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_MessageException(\r
'Data CRC check failed',\r
HTTP_Request2_Exception::DECODE_ERROR\r
);\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://pear.php.net/bugs/bug.php?id=19332\r
* @link http://tools.ietf.org/html/rfc1928\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2016 Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
- * @version Release: 2.2.1\r
+ * @version Release: 2.3.0\r
* @link http://pear.php.net/package/HTTP_Request2\r
* @link http://pear.php.net/bugs/bug.php?id=19332\r
* @link http://tools.ietf.org/html/rfc1928\r
// Backwards compatibility with 2.1.0 and 2.1.1 releases\r
$contextOptions = array('ssl' => $contextOptions);\r
}\r
+ if (isset($contextOptions['ssl'])) {\r
+ $contextOptions['ssl'] += array(\r
+ // Using "Intermediate compatibility" cipher bundle from\r
+ // https://wiki.mozilla.org/Security/Server_Side_TLS\r
+ 'ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:'\r
+ . 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:'\r
+ . 'DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:'\r
+ . 'ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:'\r
+ . 'ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:'\r
+ . 'ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:'\r
+ . 'ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:'\r
+ . 'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:'\r
+ . 'DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:'\r
+ . 'ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:'\r
+ . 'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:'\r
+ . 'AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:'\r
+ . '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'\r
+ );\r
+ if (version_compare(phpversion(), '5.4.13', '>=')) {\r
+ $contextOptions['ssl']['disable_compression'] = true;\r
+ if (version_compare(phpversion(), '5.6', '>=')) {\r
+ $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT\r
+ | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;\r
+ }\r
+ }\r
+ }\r
$context = stream_context_create();\r
foreach ($contextOptions as $wrapper => $options) {\r
foreach ($options as $name => $value) {\r
*/\r
public function enableCrypto()\r
{\r
- $modes = array(\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
- );\r
+ if (version_compare(phpversion(), '5.6', '<')) {\r
+ $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;\r
+ } else {\r
+ $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT\r
+ | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;\r
+ }\r
\r
- foreach ($modes as $mode) {\r
- if (stream_socket_enable_crypto($this->socket, true, $mode)) {\r
- return;\r
- }\r
+ if (!stream_socket_enable_crypto($this->socket, true, $cryptoMethod)) {\r
+ throw new HTTP_Request2_ConnectionException(\r
+ 'Failed to enable secure connection when connecting through proxy'\r
+ );\r
}\r
- throw new HTTP_Request2_ConnectionException(\r
- 'Failed to enable secure connection when connecting through proxy'\r
- );\r
}\r
\r
/**\r