266 lines
8.7 KiB
PHP
266 lines
8.7 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* An observer that saves response body to stream, possibly uncompressing it
|
||
|
*
|
||
|
* PHP version 5
|
||
|
*
|
||
|
* LICENSE
|
||
|
*
|
||
|
* This source file is subject to BSD 3-Clause License that is bundled
|
||
|
* with this package in the file LICENSE and available at the URL
|
||
|
* https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
|
||
|
*
|
||
|
* @category HTTP
|
||
|
* @package HTTP_Request2
|
||
|
* @author Delian Krustev <krustev@krustev.net>
|
||
|
* @author Alexey Borzov <avb@php.net>
|
||
|
* @copyright 2008-2016 Alexey Borzov <avb@php.net>
|
||
|
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
|
||
|
* @link http://pear.php.net/package/HTTP_Request2
|
||
|
*/
|
||
|
|
||
|
require_once 'HTTP/Request2/Response.php';
|
||
|
|
||
|
/**
|
||
|
* An observer that saves response body to stream, possibly uncompressing it
|
||
|
*
|
||
|
* This Observer is written in compliment to pear's HTTP_Request2 in order to
|
||
|
* avoid reading the whole response body in memory. Instead it writes the body
|
||
|
* to a stream. If the body is transferred with content-encoding set to
|
||
|
* "deflate" or "gzip" it is decoded on the fly.
|
||
|
*
|
||
|
* The constructor accepts an already opened (for write) stream (file_descriptor).
|
||
|
* If the response is deflate/gzip encoded a "zlib.inflate" filter is applied
|
||
|
* to the stream. When the body has been read from the request and written to
|
||
|
* the stream ("receivedBody" event) the filter is removed from the stream.
|
||
|
*
|
||
|
* The "zlib.inflate" filter works fine with pure "deflate" encoding. It does
|
||
|
* not understand the "deflate+zlib" and "gzip" headers though, so they have to
|
||
|
* be removed prior to being passed to the stream. This is done in the "update"
|
||
|
* method.
|
||
|
*
|
||
|
* It is also possible to limit the size of written extracted bytes by passing
|
||
|
* "max_bytes" to the constructor. This is important because e.g. 1GB of
|
||
|
* zeroes take about a MB when compressed.
|
||
|
*
|
||
|
* Exceptions are being thrown if data could not be written to the stream or
|
||
|
* the written bytes have already exceeded the requested maximum. If the "gzip"
|
||
|
* header is malformed or could not be parsed an exception will be thrown too.
|
||
|
*
|
||
|
* Example usage follows:
|
||
|
*
|
||
|
* <code>
|
||
|
* require_once 'HTTP/Request2.php';
|
||
|
* require_once 'HTTP/Request2/Observer/UncompressingDownload.php';
|
||
|
*
|
||
|
* #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html';
|
||
|
* #$inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on';
|
||
|
* $inPath = 'http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on&zlib=on';
|
||
|
* #$outPath = "/dev/null";
|
||
|
* $outPath = "delme";
|
||
|
*
|
||
|
* $stream = fopen($outPath, 'wb');
|
||
|
* if (!$stream) {
|
||
|
* throw new Exception('fopen failed');
|
||
|
* }
|
||
|
*
|
||
|
* $request = new HTTP_Request2(
|
||
|
* $inPath,
|
||
|
* HTTP_Request2::METHOD_GET,
|
||
|
* array(
|
||
|
* 'store_body' => false,
|
||
|
* 'connect_timeout' => 5,
|
||
|
* 'timeout' => 10,
|
||
|
* 'ssl_verify_peer' => true,
|
||
|
* 'ssl_verify_host' => true,
|
||
|
* 'ssl_cafile' => null,
|
||
|
* 'ssl_capath' => '/etc/ssl/certs',
|
||
|
* 'max_redirects' => 10,
|
||
|
* 'follow_redirects' => true,
|
||
|
* 'strict_redirects' => false
|
||
|
* )
|
||
|
* );
|
||
|
*
|
||
|
* $observer = new HTTP_Request2_Observer_UncompressingDownload($stream, 9999999);
|
||
|
* $request->attach($observer);
|
||
|
*
|
||
|
* $response = $request->send();
|
||
|
*
|
||
|
* fclose($stream);
|
||
|
* echo "OK\n";
|
||
|
* </code>
|
||
|
*
|
||
|
* @category HTTP
|
||
|
* @package HTTP_Request2
|
||
|
* @author Delian Krustev <krustev@krustev.net>
|
||
|
* @author Alexey Borzov <avb@php.net>
|
||
|
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
|
||
|
* @version Release: 2.3.0
|
||
|
* @link http://pear.php.net/package/HTTP_Request2
|
||
|
*/
|
||
|
class HTTP_Request2_Observer_UncompressingDownload implements SplObserver
|
||
|
{
|
||
|
/**
|
||
|
* The stream to write response body to
|
||
|
* @var resource
|
||
|
*/
|
||
|
private $_stream;
|
||
|
|
||
|
/**
|
||
|
* zlib.inflate filter possibly added to stream
|
||
|
* @var resource
|
||
|
*/
|
||
|
private $_streamFilter;
|
||
|
|
||
|
/**
|
||
|
* The value of response's Content-Encoding header
|
||
|
* @var string
|
||
|
*/
|
||
|
private $_encoding;
|
||
|
|
||
|
/**
|
||
|
* Whether the observer is still waiting for gzip/deflate header
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $_processingHeader = true;
|
||
|
|
||
|
/**
|
||
|
* Starting position in the stream observer writes to
|
||
|
* @var int
|
||
|
*/
|
||
|
private $_startPosition = 0;
|
||
|
|
||
|
/**
|
||
|
* Maximum bytes to write
|
||
|
* @var int|null
|
||
|
*/
|
||
|
private $_maxDownloadSize;
|
||
|
|
||
|
/**
|
||
|
* Whether response being received is a redirect
|
||
|
* @var bool
|
||
|
*/
|
||
|
private $_redirect = false;
|
||
|
|
||
|
/**
|
||
|
* Accumulated body chunks that may contain (gzip) header
|
||
|
* @var string
|
||
|
*/
|
||
|
private $_possibleHeader = '';
|
||
|
|
||
|
/**
|
||
|
* Class constructor
|
||
|
*
|
||
|
* Note that there might be problems with max_bytes and files bigger
|
||
|
* than 2 GB on 32bit platforms
|
||
|
*
|
||
|
* @param resource $stream a stream (or file descriptor) opened for writing.
|
||
|
* @param int $maxDownloadSize maximum bytes to write
|
||
|
*/
|
||
|
public function __construct($stream, $maxDownloadSize = null)
|
||
|
{
|
||
|
$this->_stream = $stream;
|
||
|
if ($maxDownloadSize) {
|
||
|
$this->_maxDownloadSize = $maxDownloadSize;
|
||
|
$this->_startPosition = ftell($this->_stream);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Called when the request notifies us of an event.
|
||
|
*
|
||
|
* @param SplSubject $request The HTTP_Request2 instance
|
||
|
*
|
||
|
* @return void
|
||
|
* @throws HTTP_Request2_MessageException
|
||
|
*/
|
||
|
public function update(SplSubject $request)
|
||
|
{
|
||
|
/* @var $request HTTP_Request2 */
|
||
|
$event = $request->getLastEvent();
|
||
|
$encoded = false;
|
||
|
|
||
|
/* @var $event['data'] HTTP_Request2_Response */
|
||
|
switch ($event['name']) {
|
||
|
case 'receivedHeaders':
|
||
|
$this->_processingHeader = true;
|
||
|
$this->_redirect = $event['data']->isRedirect();
|
||
|
$this->_encoding = strtolower($event['data']->getHeader('content-encoding'));
|
||
|
$this->_possibleHeader = '';
|
||
|
break;
|
||
|
|
||
|
case 'receivedEncodedBodyPart':
|
||
|
if (!$this->_streamFilter
|
||
|
&& ($this->_encoding === 'deflate' || $this->_encoding === 'gzip')
|
||
|
) {
|
||
|
$this->_streamFilter = stream_filter_append(
|
||
|
$this->_stream, 'zlib.inflate', STREAM_FILTER_WRITE
|
||
|
);
|
||
|
}
|
||
|
$encoded = true;
|
||
|
// fall-through is intentional
|
||
|
|
||
|
case 'receivedBodyPart':
|
||
|
if ($this->_redirect) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (!$encoded || !$this->_processingHeader) {
|
||
|
$bytes = fwrite($this->_stream, $event['data']);
|
||
|
|
||
|
} else {
|
||
|
$offset = 0;
|
||
|
$this->_possibleHeader .= $event['data'];
|
||
|
if ('deflate' === $this->_encoding) {
|
||
|
if (2 > strlen($this->_possibleHeader)) {
|
||
|
break;
|
||
|
}
|
||
|
$header = unpack('n', substr($this->_possibleHeader, 0, 2));
|
||
|
if (0 == $header[1] % 31) {
|
||
|
$offset = 2;
|
||
|
}
|
||
|
|
||
|
} elseif ('gzip' === $this->_encoding) {
|
||
|
if (10 > strlen($this->_possibleHeader)) {
|
||
|
break;
|
||
|
}
|
||
|
try {
|
||
|
$offset = HTTP_Request2_Response::parseGzipHeader($this->_possibleHeader, false);
|
||
|
|
||
|
} catch (HTTP_Request2_MessageException $e) {
|
||
|
// need more data?
|
||
|
if (false !== strpos($e->getMessage(), 'data too short')) {
|
||
|
break;
|
||
|
}
|
||
|
throw $e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->_processingHeader = false;
|
||
|
$bytes = fwrite($this->_stream, substr($this->_possibleHeader, $offset));
|
||
|
}
|
||
|
|
||
|
if (false === $bytes) {
|
||
|
throw new HTTP_Request2_MessageException('fwrite failed.');
|
||
|
}
|
||
|
|
||
|
if ($this->_maxDownloadSize
|
||
|
&& ftell($this->_stream) - $this->_startPosition > $this->_maxDownloadSize
|
||
|
) {
|
||
|
throw new HTTP_Request2_MessageException(sprintf(
|
||
|
'Body length limit (%d bytes) reached',
|
||
|
$this->_maxDownloadSize
|
||
|
));
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'receivedBody':
|
||
|
if ($this->_streamFilter) {
|
||
|
stream_filter_remove($this->_streamFilter);
|
||
|
$this->_streamFilter = null;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|