9 * Encode and send gzipped/deflated content
11 * The "Vary: Accept-Encoding" header is sent. If the client allows encoding,
12 * Content-Encoding and Content-Length are added.
15 * // Send a CSS file, compressed if possible
16 * $he = new HTTP_Encoder(array(
17 * 'content' => file_get_contents($cssFile)
18 * ,'type' => 'text/css'
25 * // Shortcut to encoding output
26 * header('Content-Type: text/css'); // needed if not HTML
27 * HTTP_Encoder::output($css);
31 * // Just sniff for the accepted encoding
32 * $encoding = HTTP_Encoder::getAcceptedEncoding();
35 * For more control over headers, use getHeaders() and getData() and send your
38 * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate,
39 * and gzcompress functions for gzip, deflate, and compress-encoding
44 * @author Stephen Clay <steve@mrclay.org>
49 * Should the encoder allow HTTP encoding to IE6?
51 * If you have many IE6 users and the bandwidth savings is worth troubling
52 * some of them, set this to true.
54 * By default, encoding is only offered to IE7+. When this is true,
55 * getAcceptedEncoding() will return an encoding for IE6 if its user agent
56 * string contains "SV1". This has been documented in many places as "safe",
57 * but there seem to be remaining, intermittent encoding bugs in patched
58 * IE6 on the wild web.
62 public static $encodeToIe6 = true;
66 * Default compression level for zlib operations
68 * This level is used if encode() is not given a $compressionLevel
72 public static $compressionLevel = 6;
76 * Get an HTTP Encoder object
78 * @param array $spec options
80 * 'content': (string required) content to be encoded
82 * 'type': (string) if set, the Content-Type header will have this value.
84 * 'method: (string) only set this if you are forcing a particular encoding
85 * method. If not set, the best method will be chosen by getAcceptedEncoding()
86 * The available methods are 'gzip', 'deflate', 'compress', and '' (no
89 public function __construct($spec)
91 $this->_useMbStrlen = (function_exists('mb_strlen')
92 && (ini_get('mbstring.func_overload') !== '')
93 && ((int)ini_get('mbstring.func_overload') & 2));
94 $this->_content = $spec['content'];
95 $this->_headers['Content-Length'] = $this->_useMbStrlen
96 ? (string)mb_strlen($this->_content, '8bit')
97 : (string)strlen($this->_content);
98 if (isset($spec['type'])) {
99 $this->_headers['Content-Type'] = $spec['type'];
101 if (isset($spec['method'])
102 && in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
104 $this->_encodeMethod = array($spec['method'], $spec['method']);
106 $this->_encodeMethod = self::getAcceptedEncoding();
111 * Get content in current form
113 * Call after encode() for encoded content.
117 public function getContent()
119 return $this->_content;
123 * Get array of output headers to be sent
128 * 'Content-Length' => '615'
129 * ,'Content-Encoding' => 'x-gzip'
130 * ,'Vary' => 'Accept-Encoding'
136 public function getHeaders()
138 return $this->_headers;
142 * Send output headers
144 * You must call this before headers are sent and it probably cannot be
145 * used in conjunction with zlib output buffering / mod_gzip. Errors are
146 * not handled purposefully.
150 public function sendHeaders()
152 foreach ($this->_headers as $name => $val) {
153 header($name . ': ' . $val);
158 * Send output headers and content
160 * A shortcut for sendHeaders() and echo getContent()
162 * You must call this before headers are sent and it probably cannot be
163 * used in conjunction with zlib output buffering / mod_gzip. Errors are
164 * not handled purposefully.
166 public function sendAll()
168 $this->sendHeaders();
169 echo $this->_content;
173 * Determine the client's best encoding method from the HTTP Accept-Encoding
176 * If no Accept-Encoding header is set, or the browser is IE before v6 SP2,
177 * this will return ('', ''), the "identity" encoding.
179 * A syntax-aware scan is done of the Accept-Encoding, so the method must
180 * be non 0. The methods are favored in order of gzip, deflate, then
181 * compress. Deflate is always smallest and generally faster, but is
182 * rarely sent by servers, so client support could be buggier.
184 * @param bool $allowCompress allow the older compress encoding
186 * @param bool $allowDeflate allow the more recent deflate encoding
188 * @return array two values, 1st is the actual encoding method, 2nd is the
189 * alias of that method to use in the Content-Encoding header (some browsers
190 * call gzip "x-gzip" etc.)
192 public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true)
194 // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
196 if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
197 || self::isBuggyIe())
199 return array('', '');
201 $ae = $_SERVER['HTTP_ACCEPT_ENCODING'];
202 // gzip checks (quick)
203 if (0 === strpos($ae, 'gzip,') // most browsers
204 || 0 === strpos($ae, 'deflate, gzip,') // opera
206 return array('gzip', 'gzip');
208 // gzip checks (slow)
210 '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
213 return array('gzip', $m[1]);
217 $aeRev = strrev($ae);
218 if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit
219 || 0 === strpos($aeRev, 'etalfed,') // gecko
220 || 0 === strpos($ae, 'deflate,') // opera
223 '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) {
224 return array('deflate', 'deflate');
227 if ($allowCompress && preg_match(
228 '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
231 return array('compress', $m[1]);
233 return array('', '');
237 * Encode (compress) the content
239 * If the encode method is '' (none) or compression level is 0, or the 'zlib'
240 * extension isn't loaded, we return false.
242 * Then the appropriate gz_* function is called to compress the content. If
243 * this fails, false is returned.
245 * The header "Vary: Accept-Encoding" is added. If encoding is successful,
246 * the Content-Length header is updated, and Content-Encoding is also added.
248 * @param int $compressionLevel given to zlib functions. If not given, the
249 * class default will be used.
251 * @return bool success true if the content was actually compressed
253 public function encode($compressionLevel = null)
255 if (! self::isBuggyIe()) {
256 $this->_headers['Vary'] = 'Accept-Encoding';
258 if (null === $compressionLevel) {
259 $compressionLevel = self::$compressionLevel;
261 if ('' === $this->_encodeMethod[0]
262 || ($compressionLevel == 0)
263 || !extension_loaded('zlib'))
267 if ($this->_encodeMethod[0] === 'deflate') {
268 $encoded = gzdeflate($this->_content, $compressionLevel);
269 } elseif ($this->_encodeMethod[0] === 'gzip') {
270 $encoded = gzencode($this->_content, $compressionLevel);
272 $encoded = gzcompress($this->_content, $compressionLevel);
274 if (false === $encoded) {
277 $this->_headers['Content-Length'] = $this->_useMbStrlen
278 ? (string)mb_strlen($encoded, '8bit')
279 : (string)strlen($encoded);
280 $this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
281 $this->_content = $encoded;
286 * Encode and send appropriate headers and content
288 * This is a convenience method for common use of the class
290 * @param string $content
292 * @param int $compressionLevel given to zlib functions. If not given, the
293 * class default will be used.
295 * @return bool success true if the content was actually compressed
297 public static function output($content, $compressionLevel = null)
299 if (null === $compressionLevel) {
300 $compressionLevel = self::$compressionLevel;
302 $he = new HTTP_Encoder(array('content' => $content));
303 $ret = $he->encode($compressionLevel);
309 * Is the browser an IE version earlier than 6 SP2?
313 public static function isBuggyIe()
315 if (empty($_SERVER['HTTP_USER_AGENT'])) {
318 $ua = $_SERVER['HTTP_USER_AGENT'];
319 // quick escape for non-IEs
320 if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ')
321 || false !== strpos($ua, 'Opera')) {
325 $version = (float)substr($ua, 30);
326 return self::$encodeToIe6
327 ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1')))
331 protected $_content = '';
332 protected $_headers = array();
333 protected $_encodeMethod = array('', '');
334 protected $_useMbStrlen = false;