336 lines
10 KiB
PHP
336 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 = 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;
|
||
|
}
|