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 | |||||
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 http://tools.ietf.org/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 http://tools.ietf.org/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 the value of the `Cache-Control` HTTP header. If null, the header will not be sent. |
||||
90 | * @see http://tools.ietf.org/html/rfc2616#section-14.9 |
||||
91 | */ |
||||
92 | public $cacheControlHeader = 'public, max-age=3600'; |
||||
93 | /** |
||||
94 | * @var string the name of the cache limiter to be set when [session_cache_limiter()](https://secure.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://secure.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 $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 $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')) { |
|||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||||
169 | // HTTP_IF_NONE_MATCH takes precedence over HTTP_IF_MODIFIED_SINCE |
||||
170 | // http://tools.ietf.org/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; |
|||
0 ignored issues
–
show
It seems like
Yii::app->request->heade...et('If-Modified-Since') can also be of type array ; however, parameter $datetime of strtotime() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
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()) { |
|||
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 |