initial commit
[namibia] / public / min / lib / HTTP / Encoder.php
1 <?php
2 /**
3  * Class HTTP_Encoder  
4  * @package Minify
5  * @subpackage HTTP
6  */
7  
8 /**
9  * Encode and send gzipped/deflated content
10  *
11  * The "Vary: Accept-Encoding" header is sent. If the client allows encoding, 
12  * Content-Encoding and Content-Length are added.
13  *
14  * <code>
15  * // Send a CSS file, compressed if possible
16  * $he = new HTTP_Encoder(array(
17  *     'content' => file_get_contents($cssFile)
18  *     ,'type' => 'text/css'
19  * ));
20  * $he->encode();
21  * $he->sendAll();
22  * </code>
23  *
24  * <code>
25  * // Shortcut to encoding output
26  * header('Content-Type: text/css'); // needed if not HTML
27  * HTTP_Encoder::output($css);
28  * </code>
29  * 
30  * <code>
31  * // Just sniff for the accepted encoding
32  * $encoding = HTTP_Encoder::getAcceptedEncoding();
33  * </code>
34  *
35  * For more control over headers, use getHeaders() and getData() and send your
36  * own output.
37  * 
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
40  * respectively.
41  * 
42  * @package Minify
43  * @subpackage HTTP
44  * @author Stephen Clay <steve@mrclay.org>
45  */
46 class HTTP_Encoder {
47
48     /**
49      * Should the encoder allow HTTP encoding to IE6? 
50      * 
51      * If you have many IE6 users and the bandwidth savings is worth troubling 
52      * some of them, set this to true.
53      * 
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.
59      * 
60      * @var bool
61      */
62     public static $encodeToIe6 = true;
63     
64     
65     /**
66      * Default compression level for zlib operations
67      * 
68      * This level is used if encode() is not given a $compressionLevel
69      * 
70      * @var int
71      */
72     public static $compressionLevel = 6;
73     
74
75     /**
76      * Get an HTTP Encoder object
77      * 
78      * @param array $spec options
79      * 
80      * 'content': (string required) content to be encoded
81      * 
82      * 'type': (string) if set, the Content-Type header will have this value.
83      * 
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
87      * encoding)
88      */
89     public function __construct($spec) 
90     {
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'];
100         }
101         if (isset($spec['method'])
102             && in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
103         {
104             $this->_encodeMethod = array($spec['method'], $spec['method']);
105         } else {
106             $this->_encodeMethod = self::getAcceptedEncoding();
107         }
108     }
109
110     /**
111      * Get content in current form
112      * 
113      * Call after encode() for encoded content.
114      * 
115      * @return string
116      */
117     public function getContent() 
118     {
119         return $this->_content;
120     }
121     
122     /**
123      * Get array of output headers to be sent
124      * 
125      * E.g.
126      * <code>
127      * array(
128      *     'Content-Length' => '615'
129      *     ,'Content-Encoding' => 'x-gzip'
130      *     ,'Vary' => 'Accept-Encoding'
131      * )
132      * </code>
133      *
134      * @return array 
135      */
136     public function getHeaders()
137     {
138         return $this->_headers;
139     }
140
141     /**
142      * Send output headers
143      * 
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.
147      * 
148      * @see getHeaders()
149      */
150     public function sendHeaders()
151     {
152         foreach ($this->_headers as $name => $val) {
153             header($name . ': ' . $val);
154         }
155     }
156     
157     /**
158      * Send output headers and content
159      * 
160      * A shortcut for sendHeaders() and echo getContent()
161      *
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.
165      */
166     public function sendAll()
167     {
168         $this->sendHeaders();
169         echo $this->_content;
170     }
171
172     /**
173      * Determine the client's best encoding method from the HTTP Accept-Encoding 
174      * header.
175      * 
176      * If no Accept-Encoding header is set, or the browser is IE before v6 SP2,
177      * this will return ('', ''), the "identity" encoding.
178      * 
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.
183      * 
184      * @param bool $allowCompress allow the older compress encoding
185      * 
186      * @param bool $allowDeflate allow the more recent deflate encoding
187      * 
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.)
191      */
192     public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true)
193     {
194         // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
195         
196         if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
197             || self::isBuggyIe())
198         {
199             return array('', '');
200         }
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
205         ) {
206             return array('gzip', 'gzip');
207         }
208         // gzip checks (slow)
209         if (preg_match(
210                 '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
211                 ,$ae
212                 ,$m)) {
213             return array('gzip', $m[1]);
214         }
215         if ($allowDeflate) {
216             // deflate checks    
217             $aeRev = strrev($ae);
218             if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit
219                 || 0 === strpos($aeRev, 'etalfed,') // gecko
220                 || 0 === strpos($ae, 'deflate,') // opera
221                 // slow parsing
222                 || preg_match(
223                     '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) {
224                 return array('deflate', 'deflate');
225             }
226         }
227         if ($allowCompress && preg_match(
228                 '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
229                 ,$ae
230                 ,$m)) {
231             return array('compress', $m[1]);
232         }
233         return array('', '');
234     }
235
236     /**
237      * Encode (compress) the content
238      * 
239      * If the encode method is '' (none) or compression level is 0, or the 'zlib'
240      * extension isn't loaded, we return false.
241      * 
242      * Then the appropriate gz_* function is called to compress the content. If
243      * this fails, false is returned.
244      * 
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.
247      * 
248      * @param int $compressionLevel given to zlib functions. If not given, the
249      * class default will be used.
250      * 
251      * @return bool success true if the content was actually compressed
252      */
253     public function encode($compressionLevel = null)
254     {
255         if (! self::isBuggyIe()) {
256             $this->_headers['Vary'] = 'Accept-Encoding';
257         }
258         if (null === $compressionLevel) {
259             $compressionLevel = self::$compressionLevel;
260         }
261         if ('' === $this->_encodeMethod[0]
262             || ($compressionLevel == 0)
263             || !extension_loaded('zlib'))
264         {
265             return false;
266         }
267         if ($this->_encodeMethod[0] === 'deflate') {
268             $encoded = gzdeflate($this->_content, $compressionLevel);
269         } elseif ($this->_encodeMethod[0] === 'gzip') {
270             $encoded = gzencode($this->_content, $compressionLevel);
271         } else {
272             $encoded = gzcompress($this->_content, $compressionLevel);
273         }
274         if (false === $encoded) {
275             return false;
276         }
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;
282         return true;
283     }
284     
285     /**
286      * Encode and send appropriate headers and content
287      *
288      * This is a convenience method for common use of the class
289      * 
290      * @param string $content
291      * 
292      * @param int $compressionLevel given to zlib functions. If not given, the
293      * class default will be used.
294      * 
295      * @return bool success true if the content was actually compressed
296      */
297     public static function output($content, $compressionLevel = null)
298     {
299         if (null === $compressionLevel) {
300             $compressionLevel = self::$compressionLevel;
301         }
302         $he = new HTTP_Encoder(array('content' => $content));
303         $ret = $he->encode($compressionLevel);
304         $he->sendAll();
305         return $ret;
306     }
307
308     /**
309      * Is the browser an IE version earlier than 6 SP2?
310      *
311      * @return bool
312      */
313     public static function isBuggyIe()
314     {
315         if (empty($_SERVER['HTTP_USER_AGENT'])) {
316             return false;
317         }
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')) {
322             return false;
323         }
324         // no regex = faaast
325         $version = (float)substr($ua, 30);
326         return self::$encodeToIe6
327             ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1')))
328             : ($version < 7);
329     }
330     
331     protected $_content = '';
332     protected $_headers = array();
333     protected $_encodeMethod = array('', '');
334     protected $_useMbStrlen = false;
335 }