Encoder.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. /**
  3. * Class HTTP_Encoder
  4. * @package Minify
  5. * @subpackage HTTP
  6. */
  7. /**
  8. * Encode and send gzipped/deflated content
  9. *
  10. * The "Vary: Accept-Encoding" header is sent. If the client allows encoding,
  11. * Content-Encoding and Content-Length are added.
  12. *
  13. * <code>
  14. * // Send a CSS file, compressed if possible
  15. * $he = new HTTP_Encoder(array(
  16. * 'content' => file_get_contents($cssFile)
  17. * ,'type' => 'text/css'
  18. * ));
  19. * $he->encode();
  20. * $he->sendAll();
  21. * </code>
  22. *
  23. * <code>
  24. * // Shortcut to encoding output
  25. * header('Content-Type: text/css'); // needed if not HTML
  26. * HTTP_Encoder::output($css);
  27. * </code>
  28. *
  29. * <code>
  30. * // Just sniff for the accepted encoding
  31. * $encoding = HTTP_Encoder::getAcceptedEncoding();
  32. * </code>
  33. *
  34. * For more control over headers, use getHeaders() and getData() and send your
  35. * own output.
  36. *
  37. * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate,
  38. * and gzcompress functions for gzip, deflate, and compress-encoding
  39. * respectively.
  40. *
  41. * @package Minify
  42. * @subpackage HTTP
  43. * @author Stephen Clay <steve@mrclay.org>
  44. */
  45. class HTTP_Encoder {
  46. /**
  47. * Should the encoder allow HTTP encoding to IE6?
  48. *
  49. * If you have many IE6 users and the bandwidth savings is worth troubling
  50. * some of them, set this to true.
  51. *
  52. * By default, encoding is only offered to IE7+. When this is true,
  53. * getAcceptedEncoding() will return an encoding for IE6 if its user agent
  54. * string contains "SV1". This has been documented in many places as "safe",
  55. * but there seem to be remaining, intermittent encoding bugs in patched
  56. * IE6 on the wild web.
  57. *
  58. * @var bool
  59. */
  60. public static $encodeToIe6 = true;
  61. /**
  62. * Default compression level for zlib operations
  63. *
  64. * This level is used if encode() is not given a $compressionLevel
  65. *
  66. * @var int
  67. */
  68. public static $compressionLevel = 6;
  69. /**
  70. * Get an HTTP Encoder object
  71. *
  72. * @param array $spec options
  73. *
  74. * 'content': (string required) content to be encoded
  75. *
  76. * 'type': (string) if set, the Content-Type header will have this value.
  77. *
  78. * 'method: (string) only set this if you are forcing a particular encoding
  79. * method. If not set, the best method will be chosen by getAcceptedEncoding()
  80. * The available methods are 'gzip', 'deflate', 'compress', and '' (no
  81. * encoding)
  82. */
  83. public function __construct($spec)
  84. {
  85. $this->_useMbStrlen = (function_exists('mb_strlen')
  86. && (ini_get('mbstring.func_overload') !== '')
  87. && ((int)ini_get('mbstring.func_overload') & 2));
  88. $this->_content = $spec['content'];
  89. $this->_headers['Content-Length'] = $this->_useMbStrlen
  90. ? (string)mb_strlen($this->_content, '8bit')
  91. : (string)strlen($this->_content);
  92. if (isset($spec['type'])) {
  93. $this->_headers['Content-Type'] = $spec['type'];
  94. }
  95. if (isset($spec['method'])
  96. && in_array($spec['method'], array('gzip', 'deflate', 'compress', '')))
  97. {
  98. $this->_encodeMethod = array($spec['method'], $spec['method']);
  99. } else {
  100. $this->_encodeMethod = self::getAcceptedEncoding();
  101. }
  102. }
  103. /**
  104. * Get content in current form
  105. *
  106. * Call after encode() for encoded content.
  107. *
  108. * @return string
  109. */
  110. public function getContent()
  111. {
  112. return $this->_content;
  113. }
  114. /**
  115. * Get array of output headers to be sent
  116. *
  117. * E.g.
  118. * <code>
  119. * array(
  120. * 'Content-Length' => '615'
  121. * ,'Content-Encoding' => 'x-gzip'
  122. * ,'Vary' => 'Accept-Encoding'
  123. * )
  124. * </code>
  125. *
  126. * @return array
  127. */
  128. public function getHeaders()
  129. {
  130. return $this->_headers;
  131. }
  132. /**
  133. * Send output headers
  134. *
  135. * You must call this before headers are sent and it probably cannot be
  136. * used in conjunction with zlib output buffering / mod_gzip. Errors are
  137. * not handled purposefully.
  138. *
  139. * @see getHeaders()
  140. */
  141. public function sendHeaders()
  142. {
  143. foreach ($this->_headers as $name => $val) {
  144. header($name . ': ' . $val);
  145. }
  146. }
  147. /**
  148. * Send output headers and content
  149. *
  150. * A shortcut for sendHeaders() and echo getContent()
  151. *
  152. * You must call this before headers are sent and it probably cannot be
  153. * used in conjunction with zlib output buffering / mod_gzip. Errors are
  154. * not handled purposefully.
  155. */
  156. public function sendAll()
  157. {
  158. $this->sendHeaders();
  159. echo $this->_content;
  160. }
  161. /**
  162. * Determine the client's best encoding method from the HTTP Accept-Encoding
  163. * header.
  164. *
  165. * If no Accept-Encoding header is set, or the browser is IE before v6 SP2,
  166. * this will return ('', ''), the "identity" encoding.
  167. *
  168. * A syntax-aware scan is done of the Accept-Encoding, so the method must
  169. * be non 0. The methods are favored in order of gzip, deflate, then
  170. * compress. Deflate is always smallest and generally faster, but is
  171. * rarely sent by servers, so client support could be buggier.
  172. *
  173. * @param bool $allowCompress allow the older compress encoding
  174. *
  175. * @param bool $allowDeflate allow the more recent deflate encoding
  176. *
  177. * @return array two values, 1st is the actual encoding method, 2nd is the
  178. * alias of that method to use in the Content-Encoding header (some browsers
  179. * call gzip "x-gzip" etc.)
  180. */
  181. public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true)
  182. {
  183. // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
  184. if (! isset($_SERVER['HTTP_ACCEPT_ENCODING'])
  185. || self::isBuggyIe())
  186. {
  187. return array('', '');
  188. }
  189. $ae = $_SERVER['HTTP_ACCEPT_ENCODING'];
  190. // gzip checks (quick)
  191. if (0 === strpos($ae, 'gzip,') // most browsers
  192. || 0 === strpos($ae, 'deflate, gzip,') // opera
  193. ) {
  194. return array('gzip', 'gzip');
  195. }
  196. // gzip checks (slow)
  197. if (preg_match(
  198. '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
  199. ,$ae
  200. ,$m)) {
  201. return array('gzip', $m[1]);
  202. }
  203. if ($allowDeflate) {
  204. // deflate checks
  205. $aeRev = strrev($ae);
  206. if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit
  207. || 0 === strpos($aeRev, 'etalfed,') // gecko
  208. || 0 === strpos($ae, 'deflate,') // opera
  209. // slow parsing
  210. || preg_match(
  211. '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) {
  212. return array('deflate', 'deflate');
  213. }
  214. }
  215. if ($allowCompress && preg_match(
  216. '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@'
  217. ,$ae
  218. ,$m)) {
  219. return array('compress', $m[1]);
  220. }
  221. return array('', '');
  222. }
  223. /**
  224. * Encode (compress) the content
  225. *
  226. * If the encode method is '' (none) or compression level is 0, or the 'zlib'
  227. * extension isn't loaded, we return false.
  228. *
  229. * Then the appropriate gz_* function is called to compress the content. If
  230. * this fails, false is returned.
  231. *
  232. * The header "Vary: Accept-Encoding" is added. If encoding is successful,
  233. * the Content-Length header is updated, and Content-Encoding is also added.
  234. *
  235. * @param int $compressionLevel given to zlib functions. If not given, the
  236. * class default will be used.
  237. *
  238. * @return bool success true if the content was actually compressed
  239. */
  240. public function encode($compressionLevel = null)
  241. {
  242. if (! self::isBuggyIe()) {
  243. $this->_headers['Vary'] = 'Accept-Encoding';
  244. }
  245. if (null === $compressionLevel) {
  246. $compressionLevel = self::$compressionLevel;
  247. }
  248. if ('' === $this->_encodeMethod[0]
  249. || ($compressionLevel == 0)
  250. || !extension_loaded('zlib'))
  251. {
  252. return false;
  253. }
  254. if ($this->_encodeMethod[0] === 'deflate') {
  255. $encoded = gzdeflate($this->_content, $compressionLevel);
  256. } elseif ($this->_encodeMethod[0] === 'gzip') {
  257. $encoded = gzencode($this->_content, $compressionLevel);
  258. } else {
  259. $encoded = gzcompress($this->_content, $compressionLevel);
  260. }
  261. if (false === $encoded) {
  262. return false;
  263. }
  264. $this->_headers['Content-Length'] = $this->_useMbStrlen
  265. ? (string)mb_strlen($encoded, '8bit')
  266. : (string)strlen($encoded);
  267. $this->_headers['Content-Encoding'] = $this->_encodeMethod[1];
  268. $this->_content = $encoded;
  269. return true;
  270. }
  271. /**
  272. * Encode and send appropriate headers and content
  273. *
  274. * This is a convenience method for common use of the class
  275. *
  276. * @param string $content
  277. *
  278. * @param int $compressionLevel given to zlib functions. If not given, the
  279. * class default will be used.
  280. *
  281. * @return bool success true if the content was actually compressed
  282. */
  283. public static function output($content, $compressionLevel = null)
  284. {
  285. if (null === $compressionLevel) {
  286. $compressionLevel = self::$compressionLevel;
  287. }
  288. $he = new HTTP_Encoder(array('content' => $content));
  289. $ret = $he->encode($compressionLevel);
  290. $he->sendAll();
  291. return $ret;
  292. }
  293. /**
  294. * Is the browser an IE version earlier than 6 SP2?
  295. *
  296. * @return bool
  297. */
  298. public static function isBuggyIe()
  299. {
  300. if (empty($_SERVER['HTTP_USER_AGENT'])) {
  301. return false;
  302. }
  303. $ua = $_SERVER['HTTP_USER_AGENT'];
  304. // quick escape for non-IEs
  305. if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ')
  306. || false !== strpos($ua, 'Opera')) {
  307. return false;
  308. }
  309. // no regex = faaast
  310. $version = (float)substr($ua, 30);
  311. return self::$encodeToIe6
  312. ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1')))
  313. : ($version < 7);
  314. }
  315. protected $_content = '';
  316. protected $_headers = array();
  317. protected $_encodeMethod = array('', '');
  318. protected $_useMbStrlen = false;
  319. }