327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
		
		
			
		
	
	
			327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
|   | <?php | ||
|  | /** | ||
|  |  * Class HTTP_Encoder   | ||
|  |  * @package Minify | ||
|  |  * @subpackage HTTP | ||
|  |  */ | ||
|  |   | ||
|  | /** | ||
|  |  * Encode and send gzipped/deflated content | ||
|  |  * | ||
|  |  * The "Vary: Accept-Encoding" header is sent. If the client allows encoding,  | ||
|  |  * Content-Encoding and Content-Length are added. | ||
|  |  * | ||
|  |  * <code> | ||
|  |  * // Send a CSS file, compressed if possible
 | ||
|  |  * $he = new HTTP_Encoder(array( | ||
|  |  *     'content' => file_get_contents($cssFile) | ||
|  |  *     ,'type' => 'text/css' | ||
|  |  * )); | ||
|  |  * $he->encode(); | ||
|  |  * $he->sendAll(); | ||
|  |  * </code> | ||
|  |  * | ||
|  |  * <code> | ||
|  |  * // Shortcut to encoding output
 | ||
|  |  * header('Content-Type: text/css'); // needed if not HTML
 | ||
|  |  * HTTP_Encoder::output($css); | ||
|  |  * </code> | ||
|  |  *  | ||
|  |  * <code> | ||
|  |  * // Just sniff for the accepted encoding
 | ||
|  |  * $encoding = HTTP_Encoder::getAcceptedEncoding(); | ||
|  |  * </code> | ||
|  |  * | ||
|  |  * For more control over headers, use getHeaders() and getData() and send your | ||
|  |  * own output. | ||
|  |  *  | ||
|  |  * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate,  | ||
|  |  * and gzcompress functions for gzip, deflate, and compress-encoding | ||
|  |  * respectively. | ||
|  |  *  | ||
|  |  * @package Minify | ||
|  |  * @subpackage HTTP | ||
|  |  * @author Stephen Clay <steve@mrclay.org> | ||
|  |  */ | ||
|  | class HTTP_Encoder { | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Should the encoder allow HTTP encoding to IE6?  | ||
|  |      *  | ||
|  |      * If you have many IE6 users and the bandwidth savings is worth troubling  | ||
|  |      * some of them, set this to true. | ||
|  |      *  | ||
|  |      * By default, encoding is only offered to IE7+. When this is true, | ||
|  |      * getAcceptedEncoding() will return an encoding for IE6 if its user agent | ||
|  |      * string contains "SV1". This has been documented in many places as "safe", | ||
|  |      * but there seem to be remaining, intermittent encoding bugs in patched  | ||
|  |      * IE6 on the wild web. | ||
|  |      *  | ||
|  |      * @var bool | ||
|  |      */ | ||
|  |     public static $encodeToIe6 = false; | ||
|  |      | ||
|  |      | ||
|  |     /** | ||
|  |      * Default compression level for zlib operations | ||
|  |      *  | ||
|  |      * This level is used if encode() is not given a $compressionLevel | ||
|  |      *  | ||
|  |      * @var int | ||
|  |      */ | ||
|  |     public static $compressionLevel = 6; | ||
|  |      | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get an HTTP Encoder object | ||
|  |      *  | ||
|  |      * @param array $spec options | ||
|  |      *  | ||
|  |      * 'content': (string required) content to be encoded | ||
|  |      *  | ||
|  |      * 'type': (string) if set, the Content-Type header will have this value. | ||
|  |      *  | ||
|  |      * 'method: (string) only set this if you are forcing a particular encoding | ||
|  |      * method. If not set, the best method will be chosen by getAcceptedEncoding() | ||
|  |      * The available methods are 'gzip', 'deflate', 'compress', and '' (no | ||
|  |      * encoding) | ||
|  |      *  | ||
|  |      * @return null | ||
|  |      */ | ||
|  |     public function __construct($spec)  | ||
|  |     { | ||
|  |         $this->_content = $spec['content']; | ||
|  |         $this->_headers['Content-Length'] = (string)strlen($this->_content); | ||
|  |         if (isset($spec['type'])) { | ||
|  |             $this->_headers['Content-Type'] = $spec['type']; | ||
|  |         } | ||
|  |         if (isset($spec['method']) | ||
|  |             && in_array($spec['method'], array('gzip', 'deflate', 'compress', ''))) | ||
|  |         { | ||
|  |             $this->_encodeMethod = array($spec['method'], $spec['method']); | ||
|  |         } else { | ||
|  |             $this->_encodeMethod = self::getAcceptedEncoding(); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Get content in current form | ||
|  |      *  | ||
|  |      * Call after encode() for encoded content. | ||
|  |      *  | ||
|  |      * return string | ||
|  |      */ | ||
|  |     public function getContent()  | ||
|  |     { | ||
|  |         return $this->_content; | ||
|  |     } | ||
|  |      | ||
|  |     /** | ||
|  |      * Get array of output headers to be sent | ||
|  |      *  | ||
|  |      * E.g. | ||
|  |      * <code> | ||
|  |      * array( | ||
|  |      *     'Content-Length' => '615' | ||
|  |      *     ,'Content-Encoding' => 'x-gzip' | ||
|  |      *     ,'Vary' => 'Accept-Encoding' | ||
|  |      * ) | ||
|  |      * </code> | ||
|  |      * | ||
|  |      * @return array  | ||
|  |      */ | ||
|  |     public function getHeaders() | ||
|  |     { | ||
|  |         return $this->_headers; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Send output headers | ||
|  |      *  | ||
|  |      * You must call this before headers are sent and it probably cannot be | ||
|  |      * used in conjunction with zlib output buffering / mod_gzip. Errors are | ||
|  |      * not handled purposefully. | ||
|  |      *  | ||
|  |      * @see getHeaders() | ||
|  |      *  | ||
|  |      * @return null | ||
|  |      */ | ||
|  |     public function sendHeaders() | ||
|  |     { | ||
|  |         foreach ($this->_headers as $name => $val) { | ||
|  |             header($name . ': ' . $val); | ||
|  |         } | ||
|  |     } | ||
|  |      | ||
|  |     /** | ||
|  |      * Send output headers and content | ||
|  |      *  | ||
|  |      * A shortcut for sendHeaders() and echo getContent() | ||
|  |      * | ||
|  |      * You must call this before headers are sent and it probably cannot be | ||
|  |      * used in conjunction with zlib output buffering / mod_gzip. Errors are | ||
|  |      * not handled purposefully. | ||
|  |      *  | ||
|  |      * @return null | ||
|  |      */ | ||
|  |     public function sendAll() | ||
|  |     { | ||
|  |         $this->sendHeaders(); | ||
|  |         echo $this->_content; | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Determine the client's best encoding method from the HTTP Accept-Encoding  | ||
|  |      * header. | ||
|  |      *  | ||
|  |      * If no Accept-Encoding header is set, or the browser is IE before v6 SP2, | ||
|  |      * this will return ('', ''), the "identity" encoding. | ||
|  |      *  | ||
|  |      * A syntax-aware scan is done of the Accept-Encoding, so the method must | ||
|  |      * be non 0. The methods are favored in order of gzip, deflate, then  | ||
|  |      * compress. Deflate is always smallest and generally faster, but is  | ||
|  |      * rarely sent by servers, so client support could be buggier. | ||
|  |      *  | ||
|  |      * @param bool $allowCompress allow the older compress encoding | ||
|  |      *  | ||
|  |      * @param bool $allowDeflate allow the more recent deflate encoding | ||
|  |      *  | ||
|  |      * @return array two values, 1st is the actual encoding method, 2nd is the | ||
|  |      * alias of that method to use in the Content-Encoding header (some browsers | ||
|  |      * call gzip "x-gzip" etc.) | ||
|  |      */ | ||
|  |     public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) | ||
|  |     { | ||
|  |         // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
 | ||
|  |          | ||
|  |         if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) | ||
|  |             || self::_isBuggyIe()) | ||
|  |         { | ||
|  |             return array('', ''); | ||
|  |         } | ||
|  |         $ae = $_SERVER['HTTP_ACCEPT_ENCODING']; | ||
|  |         // gzip checks (quick)
 | ||
|  |         if (0 === strpos($ae, 'gzip,')             // most browsers
 | ||
|  |             || 0 === strpos($ae, 'deflate, gzip,') // opera
 | ||
|  |         ) { | ||
|  |             return array('gzip', 'gzip'); | ||
|  |         } | ||
|  |         // gzip checks (slow)
 | ||
|  |         if (preg_match( | ||
|  |                 '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' | ||
|  |                 ,$ae | ||
|  |                 ,$m)) { | ||
|  |             return array('gzip', $m[1]); | ||
|  |         } | ||
|  |         if ($allowDeflate) { | ||
|  |             // deflate checks    
 | ||
|  |             $aeRev = strrev($ae); | ||
|  |             if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit
 | ||
|  |                 || 0 === strpos($aeRev, 'etalfed,') // gecko
 | ||
|  |                 || 0 === strpos($ae, 'deflate,') // opera
 | ||
|  |                 // slow parsing
 | ||
|  |                 || preg_match( | ||
|  |                     '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) { | ||
|  |                 return array('deflate', 'deflate'); | ||
|  |             } | ||
|  |         } | ||
|  |         if ($allowCompress && preg_match( | ||
|  |                 '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' | ||
|  |                 ,$ae | ||
|  |                 ,$m)) { | ||
|  |             return array('compress', $m[1]); | ||
|  |         } | ||
|  |         return array('', ''); | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Encode (compress) the content | ||
|  |      *  | ||
|  |      * If the encode method is '' (none) or compression level is 0, or the 'zlib' | ||
|  |      * extension isn't loaded, we return false. | ||
|  |      *  | ||
|  |      * Then the appropriate gz_* function is called to compress the content. If | ||
|  |      * this fails, false is returned. | ||
|  |      *  | ||
|  |      * The header "Vary: Accept-Encoding" is added. If encoding is successful,  | ||
|  |      * the Content-Length header is updated, and Content-Encoding is also added. | ||
|  |      *  | ||
|  |      * @param int $compressionLevel given to zlib functions. If not given, the | ||
|  |      * class default will be used. | ||
|  |      *  | ||
|  |      * @return bool success true if the content was actually compressed | ||
|  |      */ | ||
|  |     public function encode($compressionLevel = null) | ||
|  |     { | ||
|  |         $this->_headers['Vary'] = 'Accept-Encoding'; | ||
|  |         if (null === $compressionLevel) { | ||
|  |             $compressionLevel = self::$compressionLevel; | ||
|  |         } | ||
|  |         if ('' === $this->_encodeMethod[0] | ||
|  |             || ($compressionLevel == 0) | ||
|  |             || !extension_loaded('zlib')) | ||
|  |         { | ||
|  |             return false; | ||
|  |         } | ||
|  |         if ($this->_encodeMethod[0] === 'deflate') { | ||
|  |             $encoded = gzdeflate($this->_content, $compressionLevel); | ||
|  |         } elseif ($this->_encodeMethod[0] === 'gzip') { | ||
|  |             $encoded = gzencode($this->_content, $compressionLevel); | ||
|  |         } else { | ||
|  |             $encoded = gzcompress($this->_content, $compressionLevel); | ||
|  |         } | ||
|  |         if (false === $encoded) { | ||
|  |             return false; | ||
|  |         } | ||
|  |         $this->_headers['Content-Length'] = strlen($encoded); | ||
|  |         $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; | ||
|  |         $this->_content = $encoded; | ||
|  |         return true; | ||
|  |     } | ||
|  |      | ||
|  |     /** | ||
|  |      * Encode and send appropriate headers and content | ||
|  |      * | ||
|  |      * This is a convenience method for common use of the class | ||
|  |      *  | ||
|  |      * @param string $content | ||
|  |      *  | ||
|  |      * @param int $compressionLevel given to zlib functions. If not given, the | ||
|  |      * class default will be used. | ||
|  |      *  | ||
|  |      * @return bool success true if the content was actually compressed | ||
|  |      */ | ||
|  |     public static function output($content, $compressionLevel = null) | ||
|  |     { | ||
|  |         if (null === $compressionLevel) { | ||
|  |             $compressionLevel = self::$compressionLevel; | ||
|  |         } | ||
|  |         $he = new HTTP_Encoder(array('content' => $content)); | ||
|  |         $ret = $he->encode($compressionLevel); | ||
|  |         $he->sendAll(); | ||
|  |         return $ret; | ||
|  |     } | ||
|  |      | ||
|  |     protected $_content = ''; | ||
|  |     protected $_headers = array(); | ||
|  |     protected $_encodeMethod = array('', ''); | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Is the browser an IE version earlier than 6 SP2?   | ||
|  |      */ | ||
|  |     protected static function _isBuggyIe() | ||
|  |     { | ||
|  |         $ua = $_SERVER['HTTP_USER_AGENT']; | ||
|  |         // quick escape for non-IEs
 | ||
|  |         if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ') | ||
|  |             || false !== strpos($ua, 'Opera')) { | ||
|  |             return false; | ||
|  |         } | ||
|  |         // no regex = faaast
 | ||
|  |         $version = (float)substr($ua, 30);  | ||
|  |         return self::$encodeToIe6 | ||
|  |             ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1'))) | ||
|  |             : ($version < 7); | ||
|  |     } | ||
|  | } |