123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- <?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 = true;
-
-
- /**
- * 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)
- */
- public function __construct($spec)
- {
- $this->_useMbStrlen = (function_exists('mb_strlen')
- && (ini_get('mbstring.func_overload') !== '')
- && ((int)ini_get('mbstring.func_overload') & 2));
- $this->_content = $spec['content'];
- $this->_headers['Content-Length'] = $this->_useMbStrlen
- ? (string)mb_strlen($this->_content, '8bit')
- : (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()
- */
- 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.
- */
- 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)
- {
- if (! self::isBuggyIe()) {
- $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'] = $this->_useMbStrlen
- ? (string)mb_strlen($encoded, '8bit')
- : (string)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;
- }
- /**
- * Is the browser an IE version earlier than 6 SP2?
- *
- * @return bool
- */
- public static function isBuggyIe()
- {
- if (empty($_SERVER['HTTP_USER_AGENT'])) {
- return false;
- }
- $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);
- }
-
- protected $_content = '';
- protected $_headers = array();
- protected $_encodeMethod = array('', '');
- protected $_useMbStrlen = false;
- }
|