PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/library/vendors/Minify/lib/HTTP/ConditionalGet.php

http://github.com/vanillaforums/Garden
PHP | 348 lines | 148 code | 17 blank | 183 comment | 24 complexity | f9760a41a6688eca78c27e4afd760a02 MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-3-Clause, MIT
  1. <?php
  2. /**
  3. * Class HTTP_ConditionalGet
  4. * @package Minify
  5. * @subpackage HTTP
  6. */
  7. /**
  8. * Implement conditional GET via a timestamp or hash of content
  9. *
  10. * E.g. Content from DB with update time:
  11. * <code>
  12. * list($updateTime, $content) = getDbUpdateAndContent();
  13. * $cg = new HTTP_ConditionalGet(array(
  14. * 'lastModifiedTime' => $updateTime
  15. * ,'isPublic' => true
  16. * ));
  17. * $cg->sendHeaders();
  18. * if ($cg->cacheIsValid) {
  19. * exit();
  20. * }
  21. * echo $content;
  22. * </code>
  23. *
  24. * E.g. Shortcut for the above
  25. * <code>
  26. * HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache
  27. * echo $content;
  28. * </code>
  29. *
  30. * E.g. Content from DB with no update time:
  31. * <code>
  32. * $content = getContentFromDB();
  33. * $cg = new HTTP_ConditionalGet(array(
  34. * 'contentHash' => md5($content)
  35. * ));
  36. * $cg->sendHeaders();
  37. * if ($cg->cacheIsValid) {
  38. * exit();
  39. * }
  40. * echo $content;
  41. * </code>
  42. *
  43. * E.g. Static content with some static includes:
  44. * <code>
  45. * // before content
  46. * $cg = new HTTP_ConditionalGet(array(
  47. * 'lastUpdateTime' => max(
  48. * filemtime(__FILE__)
  49. * ,filemtime('/path/to/header.inc')
  50. * ,filemtime('/path/to/footer.inc')
  51. * )
  52. * ));
  53. * $cg->sendHeaders();
  54. * if ($cg->cacheIsValid) {
  55. * exit();
  56. * }
  57. * </code>
  58. * @package Minify
  59. * @subpackage HTTP
  60. * @author Stephen Clay <steve@mrclay.org>
  61. */
  62. class HTTP_ConditionalGet {
  63. /**
  64. * Does the client have a valid copy of the requested resource?
  65. *
  66. * You'll want to check this after instantiating the object. If true, do
  67. * not send content, just call sendHeaders() if you haven't already.
  68. *
  69. * @var bool
  70. */
  71. public $cacheIsValid = null;
  72. /**
  73. * @param array $spec options
  74. *
  75. * 'isPublic': (bool) if true, the Cache-Control header will contain
  76. * "public", allowing proxies to cache the content. Otherwise "private" will
  77. * be sent, allowing only browser caching. (default false)
  78. *
  79. * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
  80. * will be sent with content. This is recommended.
  81. *
  82. * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will
  83. * always be sent and a truncated version of the encoding will be appended
  84. * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient
  85. * checking of the client's If-None-Match header, as the encoding portion of
  86. * the ETag will be stripped before comparison.
  87. *
  88. * 'contentHash': (string) if given, only the ETag header can be sent with
  89. * content (only HTTP1.1 clients can conditionally GET). The given string
  90. * should be short with no quote characters and always change when the
  91. * resource changes (recommend md5()). This is not needed/used if
  92. * lastModifiedTime is given.
  93. *
  94. * 'eTag': (string) if given, this will be used as the ETag header rather
  95. * than values based on lastModifiedTime or contentHash. Also the encoding
  96. * string will not be appended to the given value as described above.
  97. *
  98. * 'invalidate': (bool) if true, the client cache will be considered invalid
  99. * without testing. Effectively this disables conditional GET.
  100. * (default false)
  101. *
  102. * 'maxAge': (int) if given, this will set the Cache-Control max-age in
  103. * seconds, and also set the Expires header to the equivalent GMT date.
  104. * After the max-age period has passed, the browser will again send a
  105. * conditional GET to revalidate its cache.
  106. *
  107. * @return null
  108. */
  109. public function __construct($spec)
  110. {
  111. $scope = (isset($spec['isPublic']) && $spec['isPublic'])
  112. ? 'public'
  113. : 'private';
  114. $maxAge = 0;
  115. // backwards compatibility (can be removed later)
  116. if (isset($spec['setExpires'])
  117. && is_numeric($spec['setExpires'])
  118. && ! isset($spec['maxAge'])) {
  119. $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME'];
  120. }
  121. if (isset($spec['maxAge'])) {
  122. $maxAge = $spec['maxAge'];
  123. $this->_headers['Expires'] = self::gmtDate(
  124. $_SERVER['REQUEST_TIME'] + $spec['maxAge']
  125. );
  126. }
  127. $etagAppend = '';
  128. if (isset($spec['encoding'])) {
  129. $this->_stripEtag = true;
  130. $this->_headers['Vary'] = 'Accept-Encoding';
  131. if ('' !== $spec['encoding']) {
  132. if (0 === strpos($spec['encoding'], 'x-')) {
  133. $spec['encoding'] = substr($spec['encoding'], 2);
  134. }
  135. $etagAppend = ';' . substr($spec['encoding'], 0, 2);
  136. }
  137. }
  138. if (isset($spec['lastModifiedTime'])) {
  139. $this->_setLastModified($spec['lastModifiedTime']);
  140. if (isset($spec['eTag'])) { // Use it
  141. $this->_setEtag($spec['eTag'], $scope);
  142. } else { // base both headers on time
  143. $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope);
  144. }
  145. } elseif (isset($spec['eTag'])) { // Use it
  146. $this->_setEtag($spec['eTag'], $scope);
  147. } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag
  148. $this->_setEtag($spec['contentHash'] . $etagAppend, $scope);
  149. }
  150. $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}";
  151. // invalidate cache if disabled, otherwise check
  152. $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
  153. ? false
  154. : $this->_isCacheValid();
  155. }
  156. /**
  157. * Get array of output headers to be sent
  158. *
  159. * In the case of 304 responses, this array will only contain the response
  160. * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified')
  161. *
  162. * Otherwise something like:
  163. * <code>
  164. * array(
  165. * 'Cache-Control' => 'max-age=0, public'
  166. * ,'ETag' => '"foobar"'
  167. * )
  168. * </code>
  169. *
  170. * @return array
  171. */
  172. public function getHeaders()
  173. {
  174. return $this->_headers;
  175. }
  176. /**
  177. * Set the Content-Length header in bytes
  178. *
  179. * With most PHP configs, as long as you don't flush() output, this method
  180. * is not needed and PHP will buffer all output and set Content-Length for
  181. * you. Otherwise you'll want to call this to let the client know up front.
  182. *
  183. * @param int $bytes
  184. *
  185. * @return int copy of input $bytes
  186. */
  187. public function setContentLength($bytes)
  188. {
  189. return $this->_headers['Content-Length'] = $bytes;
  190. }
  191. /**
  192. * Send headers
  193. *
  194. * @see getHeaders()
  195. *
  196. * Note this doesn't "clear" the headers. Calling sendHeaders() will
  197. * call header() again (but probably have not effect) and getHeaders() will
  198. * still return the headers.
  199. *
  200. * @return null
  201. */
  202. public function sendHeaders()
  203. {
  204. $headers = $this->_headers;
  205. if (array_key_exists('_responseCode', $headers)) {
  206. header($headers['_responseCode']);
  207. unset($headers['_responseCode']);
  208. }
  209. foreach ($headers as $name => $val) {
  210. header($name . ': ' . $val);
  211. }
  212. }
  213. /**
  214. * Exit if the client's cache is valid for this resource
  215. *
  216. * This is a convenience method for common use of the class
  217. *
  218. * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers
  219. * will be sent with content. This is recommended.
  220. *
  221. * @param bool $isPublic (default false) if true, the Cache-Control header
  222. * will contain "public", allowing proxies to cache the content. Otherwise
  223. * "private" will be sent, allowing only browser caching.
  224. *
  225. * @param array $options (default empty) additional options for constructor
  226. *
  227. * @return null
  228. */
  229. public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
  230. {
  231. if (null !== $lastModifiedTime) {
  232. $options['lastModifiedTime'] = (int)$lastModifiedTime;
  233. }
  234. $options['isPublic'] = (bool)$isPublic;
  235. $cg = new HTTP_ConditionalGet($options);
  236. $cg->sendHeaders();
  237. if ($cg->cacheIsValid) {
  238. exit();
  239. }
  240. }
  241. /**
  242. * Get a GMT formatted date for use in HTTP headers
  243. *
  244. * <code>
  245. * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time));
  246. * </code>
  247. *
  248. * @param int $time unix timestamp
  249. *
  250. * @return string
  251. */
  252. public static function gmtDate($time)
  253. {
  254. return gmdate('D, d M Y H:i:s \G\M\T', $time);
  255. }
  256. protected $_headers = array();
  257. protected $_lmTime = null;
  258. protected $_etag = null;
  259. protected $_stripEtag = false;
  260. protected function _setEtag($hash, $scope)
  261. {
  262. $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"';
  263. $this->_headers['ETag'] = $this->_etag;
  264. }
  265. protected function _setLastModified($time)
  266. {
  267. $this->_lmTime = (int)$time;
  268. $this->_headers['Last-Modified'] = self::gmtDate($time);
  269. }
  270. /**
  271. * Determine validity of client cache and queue 304 header if valid
  272. */
  273. protected function _isCacheValid()
  274. {
  275. if (null === $this->_etag) {
  276. // lmTime is copied to ETag, so this condition implies that the
  277. // server sent neither ETag nor Last-Modified, so the client can't
  278. // possibly has a valid cache.
  279. return false;
  280. }
  281. $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
  282. if ($isValid) {
  283. $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified';
  284. }
  285. return $isValid;
  286. }
  287. protected function resourceMatchedEtag()
  288. {
  289. if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
  290. return false;
  291. }
  292. $clientEtagList = get_magic_quotes_gpc()
  293. ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
  294. : $_SERVER['HTTP_IF_NONE_MATCH'];
  295. $clientEtags = explode(',', $clientEtagList);
  296. $compareTo = $this->normalizeEtag($this->_etag);
  297. foreach ($clientEtags as $clientEtag) {
  298. if ($this->normalizeEtag($clientEtag) === $compareTo) {
  299. // respond with the client's matched ETag, even if it's not what
  300. // we would've sent by default
  301. $this->_headers['ETag'] = trim($clientEtag);
  302. return true;
  303. }
  304. }
  305. return false;
  306. }
  307. protected function normalizeEtag($etag) {
  308. $etag = trim($etag);
  309. return $this->_stripEtag
  310. ? preg_replace('/;\\w\\w"$/', '"', $etag)
  311. : $etag;
  312. }
  313. protected function resourceNotModified()
  314. {
  315. if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  316. return false;
  317. }
  318. $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  319. if (false !== ($semicolon = strrpos($ifModifiedSince, ';'))) {
  320. // IE has tacked on extra data to this header, strip it
  321. $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon);
  322. }
  323. if ($ifModifiedSince == self::gmtDate($this->_lmTime)) {
  324. // Apache 2.2's behavior. If there was no ETag match, send the
  325. // non-encoded version of the ETag value.
  326. $this->_headers['ETag'] = $this->normalizeEtag($this->_etag);
  327. return true;
  328. }
  329. return false;
  330. }
  331. }