Issues (910)

framework/filters/PageCache.php (2 issues)

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 Closure;
11
use Yii;
12
use yii\base\Action;
13
use yii\base\ActionFilter;
14
use yii\base\DynamicContentAwareInterface;
15
use yii\base\DynamicContentAwareTrait;
16
use yii\caching\CacheInterface;
17
use yii\caching\Dependency;
18
use yii\di\Instance;
19
use yii\web\Response;
20
21
/**
22
 * PageCache implements server-side caching of whole pages.
23
 *
24
 * It is an action filter that can be added to a controller and handles the `beforeAction` event.
25
 *
26
 * To use PageCache, declare it in the `behaviors()` method of your controller class.
27
 * In the following example the filter will be applied to the `index` action and
28
 * cache the whole page for maximum 60 seconds or until the count of entries in the post table changes.
29
 * It also stores different versions of the page depending on the application language.
30
 *
31
 * ```php
32
 * public function behaviors()
33
 * {
34
 *     return [
35
 *         'pageCache' => [
36
 *             'class' => 'yii\filters\PageCache',
37
 *             'only' => ['index'],
38
 *             'duration' => 60,
39
 *             'dependency' => [
40
 *                 'class' => 'yii\caching\DbDependency',
41
 *                 'sql' => 'SELECT COUNT(*) FROM post',
42
 *             ],
43
 *             'variations' => [
44
 *                 \Yii::$app->language,
45
 *             ]
46
 *         ],
47
 *     ];
48
 * }
49
 * ```
50
 *
51
 * @author Qiang Xue <[email protected]>
52
 * @author Sergey Makinen <[email protected]>
53
 * @since 2.0
54
 */
55
class PageCache extends ActionFilter implements DynamicContentAwareInterface
56
{
57
    use DynamicContentAwareTrait;
58
59
    /**
60
     * Page cache version, to detect incompatibilities in cached values when the
61
     * data format of the cache changes.
62
     */
63
    const PAGE_CACHE_VERSION = 1;
64
65
    /**
66
     * @var bool whether the content being cached should be differentiated according to the route.
67
     * A route consists of the requested controller ID and action ID. Defaults to `true`.
68
     */
69
    public $varyByRoute = true;
70
    /**
71
     * @var CacheInterface|array|string the cache object or the application component ID of the cache object.
72
     * After the PageCache object is created, if you want to change this property,
73
     * you should only assign it with a cache object.
74
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
75
     */
76
    public $cache = 'cache';
77
    /**
78
     * @var int number of seconds that the data can remain valid in cache.
79
     * Use `0` to indicate that the cached data will never expire.
80
     */
81
    public $duration = 60;
82
    /**
83
     * @var array|Dependency the dependency that the cached content depends on.
84
     * This can be either a [[Dependency]] object or a configuration array for creating the dependency object.
85
     * For example,
86
     *
87
     * ```php
88
     * [
89
     *     'class' => 'yii\caching\DbDependency',
90
     *     'sql' => 'SELECT MAX(updated_at) FROM post',
91
     * ]
92
     * ```
93
     *
94
     * would make the output cache depend on the last modified time of all posts.
95
     * If any post has its modification time changed, the cached content would be invalidated.
96
     *
97
     * If [[cacheCookies]] or [[cacheHeaders]] is enabled, then [[\yii\caching\Dependency::reusable]] should be enabled as well to save performance.
98
     * This is because the cookies and headers are currently stored separately from the actual page content, causing the dependency to be evaluated twice.
99
     */
100
    public $dependency;
101
    /**
102
     * @var string[]|string|callable list of factors that would cause the variation of the content being cached.
103
     * Each factor is a string representing a variation (e.g. the language, a GET parameter).
104
     * The following variation setting will cause the content to be cached in different versions
105
     * according to the current application language:
106
     *
107
     * ```php
108
     * [
109
     *     Yii::$app->language,
110
     * ]
111
     * ```
112
     *
113
     * Since version 2.0.48 you can provide an anonymous function to generate variations. This is especially helpful
114
     * when you need to access the User component, which is resolved before the PageCache behavior:
115
     *
116
     * ```php
117
     * 'variations' => function() {
118
     *     return [
119
     *         Yii::$app->language,
120
     *         Yii::$app->user->id
121
     *     ];
122
     * }
123
     * ```
124
     *
125
     * The callable should return an array.
126
     */
127
    public $variations;
128
    /**
129
     * @var bool whether to enable the page cache. You may use this property to turn on and off
130
     * the page cache according to specific setting (e.g. enable page cache only for GET requests).
131
     */
132
    public $enabled = true;
133
    /**
134
     * @var \yii\base\View|null the view component to use for caching. If not set, the default application view component
135
     * [[\yii\web\Application::view]] will be used.
136
     */
137
    public $view;
138
    /**
139
     * @var bool|array a boolean value indicating whether to cache all cookies, or an array of
140
     * cookie names indicating which cookies can be cached. Be very careful with caching cookies, because
141
     * it may leak sensitive or private data stored in cookies to unwanted users.
142
     * @since 2.0.4
143
     */
144
    public $cacheCookies = false;
145
    /**
146
     * @var bool|array a boolean value indicating whether to cache all HTTP headers, or an array of
147
     * HTTP header names (case-sensitive) indicating which HTTP headers can be cached.
148
     * Note if your HTTP headers contain sensitive information, you should white-list which headers can be cached.
149
     * @since 2.0.4
150
     */
151
    public $cacheHeaders = true;
152
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 17
    public function init()
158
    {
159 17
        parent::init();
160 17
        if ($this->view === null) {
161 3
            $this->view = Yii::$app->getView();
162
        }
163
    }
164
165
    /**
166
     * This method is invoked right before an action is to be executed (after all possible filters.)
167
     * You may override this method to do last-minute preparation for the action.
168
     * @param Action $action the action to be executed.
169
     * @return bool whether the action should continue to be executed.
170
     */
171 14
    public function beforeAction($action)
172
    {
173 14
        if (!$this->enabled) {
174 1
            return true;
175
        }
176
177 13
        $this->cache = Instance::ensure($this->cache, 'yii\caching\CacheInterface');
178
179 13
        if (is_array($this->dependency)) {
180 1
            $this->dependency = Yii::createObject($this->dependency);
181
        }
182
183 13
        $response = Yii::$app->getResponse();
184 13
        $data = $this->cache->get($this->calculateCacheKey());
185 13
        if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== static::PAGE_CACHE_VERSION) {
186 13
            $this->view->pushDynamicContent($this);
187 13
            ob_start();
188 13
            ob_implicit_flush(false);
189 13
            $response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
190 13
            Yii::debug('Valid page content is not found in the cache.', __METHOD__);
191 13
            return true;
192
        }
193
194 12
        $this->restoreResponse($response, $data);
195 12
        Yii::debug('Valid page content is found in the cache.', __METHOD__);
196 12
        return false;
197
    }
198
199
    /**
200
     * This method is invoked right before the response caching is to be started.
201
     * You may override this method to cancel caching by returning `false` or store an additional data
202
     * in a cache entry by returning an array instead of `true`.
203
     * @return bool|array whether to cache or not, return an array instead of `true` to store an additional data.
204
     * @since 2.0.11
205
     */
206 13
    public function beforeCacheResponse()
207
    {
208 13
        return true;
209
    }
210
211
    /**
212
     * This method is invoked right after the response restoring is finished (but before the response is sent).
213
     * You may override this method to do last-minute preparation before the response is sent.
214
     * @param array|null $data an array of an additional data stored in a cache entry or `null`.
215
     * @since 2.0.11
216
     */
217 12
    public function afterRestoreResponse($data)
218
    {
219 12
    }
220
221
    /**
222
     * Restores response properties from the given data.
223
     * @param Response $response the response to be restored.
224
     * @param array $data the response property data.
225
     * @since 2.0.3
226
     */
227 12
    protected function restoreResponse($response, $data)
228
    {
229 12
        foreach (['format', 'version', 'statusCode', 'statusText', 'content'] as $name) {
230 12
            $response->{$name} = $data[$name];
231
        }
232 12
        foreach (['headers', 'cookies'] as $name) {
233 12
            if (isset($data[$name]) && is_array($data[$name])) {
234 11
                $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray()));
235
            }
236
        }
237 12
        if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
238 12
            $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders'], true);
239
        }
240 12
        $this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
241
    }
242
243
    /**
244
     * Caches response properties.
245
     * @since 2.0.3
246
     */
247 13
    public function cacheResponse()
248
    {
249 13
        $this->view->popDynamicContent();
250 13
        $beforeCacheResponseResult = $this->beforeCacheResponse();
251 13
        if ($beforeCacheResponseResult === false) {
252
            echo $this->updateDynamicContent(ob_get_clean(), $this->getDynamicPlaceholders());
253
            return;
254
        }
255
256 13
        $response = Yii::$app->getResponse();
257 13
        $response->off(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
258 13
        $data = [
259 13
            'cacheVersion' => static::PAGE_CACHE_VERSION,
260 13
            'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
261 13
            'content' => ob_get_clean(),
262 13
        ];
263 13
        if ($data['content'] === false || $data['content'] === '') {
264 4
            return;
265
        }
266
267 13
        $data['dynamicPlaceholders'] = $this->getDynamicPlaceholders();
268 13
        foreach (['format', 'version', 'statusCode', 'statusText'] as $name) {
269 13
            $data[$name] = $response->{$name};
270
        }
271 13
        $this->insertResponseHeaderCollectionIntoData($response, $data);
0 ignored issues
show
It seems like $response can also be of type yii\console\Response; however, parameter $response of yii\filters\PageCache::i...derCollectionIntoData() does only seem to accept yii\web\Response, 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 ignore-type  annotation

271
        $this->insertResponseHeaderCollectionIntoData(/** @scrutinizer ignore-type */ $response, $data);
Loading history...
272 13
        $this->insertResponseCookieCollectionIntoData($response, $data);
0 ignored issues
show
It seems like $response can also be of type yii\console\Response; however, parameter $response of yii\filters\PageCache::i...kieCollectionIntoData() does only seem to accept yii\web\Response, 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 ignore-type  annotation

272
        $this->insertResponseCookieCollectionIntoData(/** @scrutinizer ignore-type */ $response, $data);
Loading history...
273 13
        $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
274 13
        $data['content'] = $this->updateDynamicContent($data['content'], $this->getDynamicPlaceholders());
275 13
        echo $data['content'];
276
    }
277
278
    /**
279
     * Inserts (or filters/ignores according to config) response cookies into a cache data array.
280
     * @param Response $response the response.
281
     * @param array $data the cache data.
282
     */
283 13
    private function insertResponseCookieCollectionIntoData(Response $response, array &$data)
284
    {
285 13
        if ($this->cacheCookies === false) {
286 10
            return;
287
        }
288
289 3
        $all = $response->cookies->toArray();
290 3
        if (is_array($this->cacheCookies)) {
291 2
            $filtered = [];
292 2
            foreach ($this->cacheCookies as $name) {
293 2
                if (isset($all[$name])) {
294 2
                    $filtered[$name] = $all[$name];
295
                }
296
            }
297 2
            $all = $filtered;
298
        }
299 3
        $data['cookies'] = $all;
300
    }
301
302
    /**
303
     * Inserts (or filters/ignores according to config) response headers into a cache data array.
304
     * @param Response $response the response.
305
     * @param array $data the cache data.
306
     */
307 13
    private function insertResponseHeaderCollectionIntoData(Response $response, array &$data)
308
    {
309 13
        if ($this->cacheHeaders === false) {
310 1
            return;
311
        }
312
313 12
        $all = $response->headers->toOriginalArray();
314 12
        if (is_array($this->cacheHeaders)) {
315 3
            $filtered = [];
316 3
            foreach ($this->cacheHeaders as $name) {
317 3
                if (isset($all[$name])) {
318 3
                    $filtered[$name] = $all[$name];
319
                }
320
            }
321 3
            $all = $filtered;
322
        }
323 12
        $data['headers'] = $all;
324
    }
325
326
    /**
327
     * @return array the key used to cache response properties.
328
     * @since 2.0.3
329
     */
330 15
    protected function calculateCacheKey()
331
    {
332 15
        $key = [__CLASS__];
333 15
        if ($this->varyByRoute) {
334 15
            $key[] = Yii::$app->requestedRoute;
335
        }
336
337 15
        if ($this->variations instanceof Closure) {
338 1
            $variations = call_user_func($this->variations, $this);
339
        } else {
340 14
            $variations = $this->variations;
341
        }
342 15
        return array_merge($key, (array) $variations);
343
    }
344
345
    /**
346
     * {@inheritdoc}
347
     */
348 13
    public function getView()
349
    {
350 13
        return $this->view;
351
    }
352
}
353