PageRenderTime 40ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/w3-total-cache/lib/Minify/HTTP/ConditionalGet.php

https://gitlab.com/juanito.abelo/nlmobile
PHP | 402 lines | 183 code | 33 blank | 186 comment | 35 complexity | eff68a0143ad207ce580a4ede79ecff4 MD5 | raw file
  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. /**
  65. * Does the client have a valid copy of the requested resource?
  66. *
  67. * You'll want to check this after instantiating the object. If true, do
  68. * not send content, just call sendHeaders() if you haven't already.
  69. *
  70. * @var bool
  71. */
  72. public $cacheIsValid = null;
  73. /**
  74. * @param array $spec options
  75. *
  76. * 'isPublic': (bool) if true, the Cache-Control header will contain
  77. * "public", allowing proxies to cache the content. Otherwise "private" will
  78. * be sent, allowing only browser caching. (default false)
  79. *
  80. * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
  81. * will be sent with content. This is recommended.
  82. *
  83. * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will
  84. * always be sent and a truncated version of the encoding will be appended
  85. * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient
  86. * checking of the client's If-None-Match header, as the encoding portion of
  87. * the ETag will be stripped before comparison.
  88. *
  89. * 'contentHash': (string) if given, only the ETag header can be sent with
  90. * content (only HTTP1.1 clients can conditionally GET). The given string
  91. * should be short with no quote characters and always change when the
  92. * resource changes (recommend md5()). This is not needed/used if
  93. * lastModifiedTime is given.
  94. *
  95. * 'eTag': (string) if given, this will be used as the ETag header rather
  96. * than values based on lastModifiedTime or contentHash. Also the encoding
  97. * string will not be appended to the given value as described above.
  98. *
  99. * 'invalidate': (bool) if true, the client cache will be considered invalid
  100. * without testing. Effectively this disables conditional GET.
  101. * (default false)
  102. *
  103. * 'maxAge': (int) if given, this will set the Cache-Control max-age in
  104. * seconds, and also set the Expires header to the equivalent GMT date.
  105. * After the max-age period has passed, the browser will again send a
  106. * conditional GET to revalidate its cache.
  107. *
  108. * @return null
  109. */
  110. public function __construct($spec)
  111. {
  112. if (isset($spec['cacheHeaders']) && is_array($spec['cacheHeaders'])) {
  113. $this->_cacheHeaders = $spec['cacheHeaders'];
  114. }
  115. $scope = ($this->_cacheHeaders['cacheheaders_enabled'] && $this->_cacheHeaders['cacheheaders'] != 'no_cache') ? 'public' : 'private';
  116. $maxAge = 0;
  117. $this->_headers['Pragma'] = $scope;
  118. // backwards compatibility (can be removed later)
  119. if (isset($spec['setExpires']) && is_numeric($spec['setExpires']) && !isset($spec['maxAge'])) {
  120. $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME'];
  121. }
  122. if (isset($spec['maxAge'])) {
  123. $maxAge = $spec['maxAge'];
  124. if ($this->_cacheHeaders['expires_enabled'] && $maxAge) {
  125. $this->_headers['Expires'] = self::gmtDate($_SERVER['REQUEST_TIME'] + $maxAge);
  126. }
  127. }
  128. $etagAppend = '';
  129. if (isset($spec['encoding'])) {
  130. $this->_stripEtag = true;
  131. $this->_headers['Vary'] = 'Accept-Encoding';
  132. if ('' !== $spec['encoding']) {
  133. if (0 === strpos($spec['encoding'], 'x-')) {
  134. $spec['encoding'] = substr($spec['encoding'], 2);
  135. }
  136. $etagAppend = ';' . substr($spec['encoding'], 0, 2);
  137. }
  138. }
  139. if (isset($spec['lastModifiedTime'])) {
  140. $this->_setLastModified($spec['lastModifiedTime']);
  141. if (isset($spec['eTag'])) { // Use it
  142. $this->_setEtag($spec['eTag'], $scope);
  143. } else { // base both headers on time
  144. $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope);
  145. }
  146. } elseif (isset($spec['eTag'])) { // Use it
  147. $this->_setEtag($spec['eTag'], $scope);
  148. } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag
  149. $this->_setEtag($spec['contentHash'] . $etagAppend, $scope);
  150. }
  151. if ($this->_cacheHeaders['cacheheaders_enabled']) {
  152. switch ($this->_cacheHeaders['cacheheaders']) {
  153. case 'cache':
  154. $this->_headers['Cache-Control'] = 'public';
  155. break;
  156. case 'cache_public_maxage':
  157. $this->_headers['Cache-Control'] = "max-age={$maxAge}, public";
  158. break;
  159. case 'cache_validation':
  160. $this->_headers['Cache-Control'] = 'public, must-revalidate, proxy-revalidate';
  161. break;
  162. case 'cache_noproxy':
  163. $this->_headers['Cache-Control'] = 'private, must-revalidate';
  164. break;
  165. case 'cache_maxage':
  166. $this->_headers['Cache-Control'] = "max-age={$maxAge}, {$scope}, must-revalidate, proxy-revalidate";
  167. break;
  168. case 'no_cache':
  169. $this->_headers['Cache-Control'] = 'max-age=0, private, no-store, no-cache, must-revalidate';
  170. break;
  171. }
  172. }
  173. /**
  174. * Disable caching for preview mode
  175. */
  176. if (w3_is_preview_mode()) {
  177. $this->_headers = array_merge($this->_headers, array(
  178. 'Pragma' => 'private',
  179. 'Cache-Control' => 'private'
  180. ));
  181. }
  182. // invalidate cache if disabled, otherwise check
  183. $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) ? false : $this->_isCacheValid();
  184. }
  185. /**
  186. * Get array of output headers to be sent
  187. *
  188. * In the case of 304 responses, this array will only contain the response
  189. * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified')
  190. *
  191. * Otherwise something like:
  192. * <code>
  193. * array(
  194. * 'Cache-Control' => 'max-age=0, public'
  195. * ,'ETag' => '"foobar"'
  196. * )
  197. * </code>
  198. *
  199. * @return array
  200. */
  201. public function getHeaders()
  202. {
  203. return $this->_headers;
  204. }
  205. /**
  206. * Set the Content-Length header in bytes
  207. *
  208. * With most PHP configs, as long as you don't flush() output, this method
  209. * is not needed and PHP will buffer all output and set Content-Length for
  210. * you. Otherwise you'll want to call this to let the client know up front.
  211. *
  212. * @param int $bytes
  213. *
  214. * @return int copy of input $bytes
  215. */
  216. public function setContentLength($bytes)
  217. {
  218. return $this->_headers['Content-Length'] = $bytes;
  219. }
  220. /**
  221. * Send headers
  222. *
  223. * @see getHeaders()
  224. *
  225. * Note this doesn't "clear" the headers. Calling sendHeaders() will
  226. * call header() again (but probably have not effect) and getHeaders() will
  227. * still return the headers.
  228. *
  229. * @return null
  230. */
  231. public function sendHeaders()
  232. {
  233. $headers = $this->_headers;
  234. if (array_key_exists('_responseCode', $headers)) {
  235. header($headers['_responseCode']);
  236. unset($headers['_responseCode']);
  237. }
  238. foreach ($headers as $name => $val) {
  239. header($name . ': ' . $val);
  240. }
  241. }
  242. /**
  243. * Exit if the client's cache is valid for this resource
  244. *
  245. * This is a convenience method for common use of the class
  246. *
  247. * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers
  248. * will be sent with content. This is recommended.
  249. *
  250. * @param bool $isPublic (default false) if true, the Cache-Control header
  251. * will contain "public", allowing proxies to cache the content. Otherwise
  252. * "private" will be sent, allowing only browser caching.
  253. *
  254. * @param array $options (default empty) additional options for constructor
  255. *
  256. * @return null
  257. */
  258. public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
  259. {
  260. if (null !== $lastModifiedTime) {
  261. $options['lastModifiedTime'] = (int) $lastModifiedTime;
  262. }
  263. $options['isPublic'] = (bool) $isPublic;
  264. $cg = new HTTP_ConditionalGet($options);
  265. $cg->sendHeaders();
  266. if ($cg->cacheIsValid) {
  267. exit();
  268. }
  269. }
  270. /**
  271. * Get a GMT formatted date for use in HTTP headers
  272. *
  273. * <code>
  274. * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time));
  275. * </code>
  276. *
  277. * @param int $time unix timestamp
  278. *
  279. * @return string
  280. */
  281. public static function gmtDate($time)
  282. {
  283. return gmdate('D, d M Y H:i:s \G\M\T', $time);
  284. }
  285. protected $_headers = array();
  286. protected $_lmTime = null;
  287. protected $_etag = null;
  288. protected $_stripEtag = false;
  289. protected $_cacheHeaders = array(
  290. 'use_etag' => true,
  291. 'expires_enabled' => true,
  292. 'cacheheaders_enabled' => true,
  293. 'cacheheaders' => 'cache_validation'
  294. );
  295. protected function _setEtag($hash, $scope)
  296. {
  297. $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"';
  298. if ($this->_cacheHeaders['use_etag']) {
  299. $this->_headers['ETag'] = $this->_etag;
  300. }
  301. }
  302. protected function _setLastModified($time)
  303. {
  304. $this->_lmTime = (int) $time;
  305. $this->_headers['Last-Modified'] = self::gmtDate($time);
  306. }
  307. /**
  308. * Determine validity of client cache and queue 304 header if valid
  309. */
  310. protected function _isCacheValid()
  311. {
  312. if (null === $this->_etag) {
  313. // lmTime is copied to ETag, so this condition implies that the
  314. // server sent neither ETag nor Last-Modified, so the client can't
  315. // possibly has a valid cache.
  316. return false;
  317. }
  318. $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
  319. if ($isValid) {
  320. $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified';
  321. }
  322. return $isValid;
  323. }
  324. protected function resourceMatchedEtag()
  325. {
  326. if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
  327. return false;
  328. }
  329. $clientEtagList = get_magic_quotes_gpc() ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : $_SERVER['HTTP_IF_NONE_MATCH'];
  330. $clientEtags = explode(',', $clientEtagList);
  331. $compareTo = $this->normalizeEtag($this->_etag);
  332. foreach ($clientEtags as $clientEtag) {
  333. if ($this->normalizeEtag($clientEtag) === $compareTo) {
  334. // respond with the client's matched ETag, even if it's not what
  335. // we would've sent by default
  336. if ($this->_cacheHeaders['use_etag']) {
  337. $this->_headers['ETag'] = trim($clientEtag);
  338. }
  339. return true;
  340. }
  341. }
  342. return false;
  343. }
  344. protected function normalizeEtag($etag)
  345. {
  346. $etag = trim($etag);
  347. return $this->_stripEtag ? preg_replace('/;\\w\\w"$/', '"', $etag) : $etag;
  348. }
  349. protected function resourceNotModified()
  350. {
  351. if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
  352. return false;
  353. }
  354. $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
  355. if (false !== ($semicolon = strrpos($ifModifiedSince, ';'))) {
  356. // IE has tacked on extra data to this header, strip it
  357. $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon);
  358. }
  359. if ($ifModifiedSince == self::gmtDate($this->_lmTime)) {
  360. // Apache 2.2's behavior. If there was no ETag match, send the
  361. // non-encoded version of the ETag value.
  362. if ($this->_cacheHeaders['use_etag']) {
  363. $this->_headers['ETag'] = $this->normalizeEtag($this->_etag);
  364. }
  365. return true;
  366. }
  367. return false;
  368. }
  369. }