123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- <?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;
- }
- }
- }
|