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