HTTP_ConditionalGet

Implement conditional GET via a timestamp or hash of content.

Defined (1)

The class is defined in the following location(s).

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