forked from GNUsocial/gnu-social
		
	
		
			
				
	
	
		
			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;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |