HttpCache.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\filters;
  8. use Yii;
  9. use yii\base\Action;
  10. use yii\base\ActionFilter;
  11. /**
  12. * HttpCache implements client-side caching by utilizing the `Last-Modified` and `ETag` HTTP headers.
  13. *
  14. * It is an action filter that can be added to a controller and handles the `beforeAction` event.
  15. *
  16. * To use HttpCache, declare it in the `behaviors()` method of your controller class.
  17. * In the following example the filter will be applied to the `list`-action and
  18. * the Last-Modified header will contain the date of the last update to the user table in the database.
  19. *
  20. * ```php
  21. * public function behaviors()
  22. * {
  23. * return [
  24. * [
  25. * 'class' => 'yii\filters\HttpCache',
  26. * 'only' => ['index'],
  27. * 'lastModified' => function ($action, $params) {
  28. * $q = new \yii\db\Query();
  29. * return $q->from('user')->max('updated_at');
  30. * },
  31. * // 'etagSeed' => function ($action, $params) {
  32. * // return // generate ETag seed here
  33. * // }
  34. * ],
  35. * ];
  36. * }
  37. * ```
  38. *
  39. * @author Da:Sourcerer <webmaster@dasourcerer.net>
  40. * @author Qiang Xue <qiang.xue@gmail.com>
  41. * @since 2.0
  42. */
  43. class HttpCache extends ActionFilter
  44. {
  45. /**
  46. * @var callable a PHP callback that returns the UNIX timestamp of the last modification time.
  47. * The callback's signature should be:
  48. *
  49. * ```php
  50. * function ($action, $params)
  51. * ```
  52. *
  53. * where `$action` is the [[Action]] object that this filter is currently handling;
  54. * `$params` takes the value of [[params]]. The callback should return a UNIX timestamp.
  55. *
  56. * @see http://tools.ietf.org/html/rfc7232#section-2.2
  57. */
  58. public $lastModified;
  59. /**
  60. * @var callable a PHP callback that generates the ETag seed string.
  61. * The callback's signature should be:
  62. *
  63. * ```php
  64. * function ($action, $params)
  65. * ```
  66. *
  67. * where `$action` is the [[Action]] object that this filter is currently handling;
  68. * `$params` takes the value of [[params]]. The callback should return a string serving
  69. * as the seed for generating an ETag.
  70. */
  71. public $etagSeed;
  72. /**
  73. * @var bool whether to generate weak ETags.
  74. *
  75. * Weak ETags should be used if the content should be considered semantically equivalent, but not byte-equal.
  76. *
  77. * @since 2.0.8
  78. * @see http://tools.ietf.org/html/rfc7232#section-2.3
  79. */
  80. public $weakEtag = false;
  81. /**
  82. * @var mixed additional parameters that should be passed to the [[lastModified]] and [[etagSeed]] callbacks.
  83. */
  84. public $params;
  85. /**
  86. * @var string the value of the `Cache-Control` HTTP header. If null, the header will not be sent.
  87. * @see http://tools.ietf.org/html/rfc2616#section-14.9
  88. */
  89. public $cacheControlHeader = 'public, max-age=3600';
  90. /**
  91. * @var string the name of the cache limiter to be set when [session_cache_limiter()](http://www.php.net/manual/en/function.session-cache-limiter.php)
  92. * is called. The default value is an empty string, meaning turning off automatic sending of cache headers entirely.
  93. * You may set this property to be `public`, `private`, `private_no_expire`, and `nocache`.
  94. * Please refer to [session_cache_limiter()](http://www.php.net/manual/en/function.session-cache-limiter.php)
  95. * for detailed explanation of these values.
  96. *
  97. * If this property is `null`, then `session_cache_limiter()` will not be called. As a result,
  98. * PHP will send headers according to the `session.cache_limiter` PHP ini setting.
  99. */
  100. public $sessionCacheLimiter = '';
  101. /**
  102. * @var bool a value indicating whether this filter should be enabled.
  103. */
  104. public $enabled = true;
  105. /**
  106. * This method is invoked right before an action is to be executed (after all possible filters.)
  107. * You may override this method to do last-minute preparation for the action.
  108. * @param Action $action the action to be executed.
  109. * @return bool whether the action should continue to be executed.
  110. */
  111. public function beforeAction($action)
  112. {
  113. if (!$this->enabled) {
  114. return true;
  115. }
  116. $verb = Yii::$app->getRequest()->getMethod();
  117. if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) {
  118. return true;
  119. }
  120. $lastModified = $etag = null;
  121. if ($this->lastModified !== null) {
  122. $lastModified = call_user_func($this->lastModified, $action, $this->params);
  123. }
  124. if ($this->etagSeed !== null) {
  125. $seed = call_user_func($this->etagSeed, $action, $this->params);
  126. if ($seed !== null) {
  127. $etag = $this->generateEtag($seed);
  128. }
  129. }
  130. $this->sendCacheControlHeader();
  131. $response = Yii::$app->getResponse();
  132. if ($etag !== null) {
  133. $response->getHeaders()->set('Etag', $etag);
  134. }
  135. $cacheValid = $this->validateCache($lastModified, $etag);
  136. // https://tools.ietf.org/html/rfc7232#section-4.1
  137. if ($lastModified !== null && (!$cacheValid || ($cacheValid && $etag === null))) {
  138. $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
  139. }
  140. if ($cacheValid) {
  141. $response->setStatusCode(304);
  142. return false;
  143. }
  144. return true;
  145. }
  146. /**
  147. * Validates if the HTTP cache contains valid content.
  148. * If both Last-Modified and ETag are null, returns false.
  149. * @param int $lastModified the calculated Last-Modified value in terms of a UNIX timestamp.
  150. * If null, the Last-Modified header will not be validated.
  151. * @param string $etag the calculated ETag value. If null, the ETag header will not be validated.
  152. * @return bool whether the HTTP cache is still valid.
  153. */
  154. protected function validateCache($lastModified, $etag)
  155. {
  156. if (Yii::$app->request->headers->has('If-None-Match')) {
  157. // HTTP_IF_NONE_MATCH takes precedence over HTTP_IF_MODIFIED_SINCE
  158. // http://tools.ietf.org/html/rfc7232#section-3.3
  159. return $etag !== null && in_array($etag, Yii::$app->request->getETags(), true);
  160. } elseif (Yii::$app->request->headers->has('If-Modified-Since')) {
  161. return $lastModified !== null && @strtotime(Yii::$app->request->headers->get('If-Modified-Since')) >= $lastModified;
  162. }
  163. return false;
  164. }
  165. /**
  166. * Sends the cache control header to the client.
  167. * @see cacheControlHeader
  168. */
  169. protected function sendCacheControlHeader()
  170. {
  171. if ($this->sessionCacheLimiter !== null) {
  172. if ($this->sessionCacheLimiter === '' && !headers_sent() && Yii::$app->getSession()->getIsActive()) {
  173. header_remove('Expires');
  174. header_remove('Cache-Control');
  175. header_remove('Last-Modified');
  176. header_remove('Pragma');
  177. }
  178. Yii::$app->getSession()->setCacheLimiter($this->sessionCacheLimiter);
  179. }
  180. $headers = Yii::$app->getResponse()->getHeaders();
  181. if ($this->cacheControlHeader !== null) {
  182. $headers->set('Cache-Control', $this->cacheControlHeader);
  183. }
  184. }
  185. /**
  186. * Generates an ETag from the given seed string.
  187. * @param string $seed Seed for the ETag
  188. * @return string the generated ETag
  189. */
  190. protected function generateEtag($seed)
  191. {
  192. $etag = '"' . rtrim(base64_encode(sha1($seed, true)), '=') . '"';
  193. return $this->weakEtag ? 'W/' . $etag : $etag;
  194. }
  195. }