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