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