ca9a73371ab900fd6032b21d4e72470b3849c852
[yaffs-website] / web / core / modules / page_cache / src / StackMiddleware / PageCache.php
1 <?php
2
3 namespace Drupal\page_cache\StackMiddleware;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheableResponseInterface;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\PageCache\RequestPolicyInterface;
9 use Drupal\Core\PageCache\ResponsePolicyInterface;
10 use Drupal\Core\Site\Settings;
11 use Symfony\Component\HttpFoundation\BinaryFileResponse;
12 use Symfony\Component\HttpFoundation\Request;
13 use Symfony\Component\HttpFoundation\Response;
14 use Symfony\Component\HttpFoundation\StreamedResponse;
15 use Symfony\Component\HttpKernel\HttpKernelInterface;
16
17 /**
18  * Executes the page caching before the main kernel takes over the request.
19  */
20 class PageCache implements HttpKernelInterface {
21
22   /**
23    * The wrapped HTTP kernel.
24    *
25    * @var \Symfony\Component\HttpKernel\HttpKernelInterface
26    */
27   protected $httpKernel;
28
29   /**
30    * The cache bin.
31    *
32    * @var \Drupal\Core\Cache\CacheBackendInterface
33    */
34   protected $cache;
35
36   /**
37    * A policy rule determining the cacheability of a request.
38    *
39    * @var \Drupal\Core\PageCache\RequestPolicyInterface
40    */
41   protected $requestPolicy;
42
43   /**
44    * A policy rule determining the cacheability of the response.
45    *
46    * @var \Drupal\Core\PageCache\ResponsePolicyInterface
47    */
48   protected $responsePolicy;
49
50   /**
51    * Constructs a PageCache object.
52    *
53    * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
54    *   The decorated kernel.
55    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
56    *   The cache bin.
57    * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
58    *   A policy rule determining the cacheability of a request.
59    * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
60    *   A policy rule determining the cacheability of the response.
61    */
62   public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) {
63     $this->httpKernel = $http_kernel;
64     $this->cache = $cache;
65     $this->requestPolicy = $request_policy;
66     $this->responsePolicy = $response_policy;
67   }
68
69   /**
70    * {@inheritdoc}
71    */
72   public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
73     // Only allow page caching on master request.
74     if ($type === static::MASTER_REQUEST && $this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) {
75       $response = $this->lookup($request, $type, $catch);
76     }
77     else {
78       $response = $this->pass($request, $type, $catch);
79     }
80
81     return $response;
82   }
83
84   /**
85    * Sidesteps the page cache and directly forwards a request to the backend.
86    *
87    * @param \Symfony\Component\HttpFoundation\Request $request
88    *   A request object.
89    * @param int $type
90    *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
91    *   HttpKernelInterface::SUB_REQUEST)
92    * @param bool $catch
93    *   Whether to catch exceptions or not
94    *
95    * @returns \Symfony\Component\HttpFoundation\Response $response
96    *   A response object.
97    */
98   protected function pass(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
99     return $this->httpKernel->handle($request, $type, $catch);
100   }
101
102   /**
103    * Retrieves a response from the cache or fetches it from the backend.
104    *
105    * @param \Symfony\Component\HttpFoundation\Request $request
106    *   A request object.
107    * @param int $type
108    *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
109    *   HttpKernelInterface::SUB_REQUEST)
110    * @param bool $catch
111    *   Whether to catch exceptions or not
112    *
113    * @returns \Symfony\Component\HttpFoundation\Response $response
114    *   A response object.
115    */
116   protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
117     if ($response = $this->get($request)) {
118       $response->headers->set('X-Drupal-Cache', 'HIT');
119     }
120     else {
121       $response = $this->fetch($request, $type, $catch);
122     }
123
124     // Only allow caching in the browser and prevent that the response is stored
125     // by an external proxy server when the following conditions apply:
126     // 1. There is a session cookie on the request.
127     // 2. The Vary: Cookie header is on the response.
128     // 3. The Cache-Control header does not contain the no-cache directive.
129     if ($request->cookies->has(session_name()) &&
130       in_array('Cookie', $response->getVary()) &&
131       !$response->headers->hasCacheControlDirective('no-cache')) {
132
133       $response->setPrivate();
134     }
135
136     // Perform HTTP revalidation.
137     // @todo Use Response::isNotModified() as
138     //   per https://www.drupal.org/node/2259489.
139     $last_modified = $response->getLastModified();
140     if ($last_modified) {
141       // See if the client has provided the required HTTP headers.
142       $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
143       $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
144
145       if ($if_modified_since && $if_none_match
146         // etag must match.
147         && $if_none_match == $response->getEtag()
148         // if-modified-since must match.
149         && $if_modified_since == $last_modified->getTimestamp()) {
150         $response->setStatusCode(304);
151         $response->setContent(NULL);
152
153         // In the case of a 304 response, certain headers must be sent, and the
154         // remaining may not (see RFC 2616, section 10.3.5).
155         foreach (array_keys($response->headers->all()) as $name) {
156           if (!in_array($name, ['content-location', 'expires', 'cache-control', 'vary'])) {
157             $response->headers->remove($name);
158           }
159         }
160       }
161     }
162
163     return $response;
164   }
165
166   /**
167    * Fetches a response from the backend and stores it in the cache.
168    *
169    * @see drupal_page_header()
170    *
171    * @param \Symfony\Component\HttpFoundation\Request $request
172    *   A request object.
173    * @param int $type
174    *   The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
175    *   HttpKernelInterface::SUB_REQUEST)
176    * @param bool $catch
177    *   Whether to catch exceptions or not
178    *
179    * @returns \Symfony\Component\HttpFoundation\Response $response
180    *   A response object.
181    */
182   protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
183     /** @var \Symfony\Component\HttpFoundation\Response $response */
184     $response = $this->httpKernel->handle($request, $type, $catch);
185
186     // Only set the 'X-Drupal-Cache' header if caching is allowed for this
187     // response.
188     if ($this->storeResponse($request, $response)) {
189       $response->headers->set('X-Drupal-Cache', 'MISS');
190     }
191
192     return $response;
193   }
194
195   /**
196    * Stores a response in the page cache.
197    *
198    * @param \Symfony\Component\HttpFoundation\Request $request
199    *   A request object.
200    * @param \Symfony\Component\HttpFoundation\Response $response
201    *   A response object that should be stored in the page cache.
202    *
203    * @returns bool
204    */
205   protected function storeResponse(Request $request, Response $response) {
206     // Drupal's primary cache invalidation architecture is cache tags: any
207     // response that varies by a configuration value or data in a content
208     // entity should have cache tags, to allow for instant cache invalidation
209     // when that data is updated. However, HTTP does not standardize how to
210     // encode cache tags in a response. Different CDNs implement their own
211     // approaches, and configurable reverse proxies (e.g., Varnish) allow for
212     // custom implementations. To keep Drupal's internal page cache simple, we
213     // only cache CacheableResponseInterface responses, since those provide a
214     // defined API for retrieving cache tags. For responses that do not
215     // implement CacheableResponseInterface, there's no easy way to distinguish
216     // responses that truly don't depend on any site data from responses that
217     // contain invalidation information customized to a particular proxy or
218     // CDN.
219     // - Drupal modules are encouraged to use CacheableResponseInterface
220     //   responses where possible and to leave the encoding of that information
221     //   into response headers to the corresponding proxy/CDN integration
222     //   modules.
223     // - Custom applications that wish to provide internal page cache support
224     //   for responses that do not implement CacheableResponseInterface may do
225     //   so by replacing/extending this middleware service or adding another
226     //   one.
227     if (!$response instanceof CacheableResponseInterface) {
228       return FALSE;
229     }
230
231     // Currently it is not possible to cache binary file or streamed responses:
232     // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678.
233     // Therefore exclude them, even for subclasses that implement
234     // CacheableResponseInterface.
235     if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) {
236       return FALSE;
237     }
238
239     // Allow policy rules to further restrict which responses to cache.
240     if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
241       return FALSE;
242     }
243
244     $request_time = $request->server->get('REQUEST_TIME');
245     // The response passes all of the above checks, so cache it. Page cache
246     // entries default to Cache::PERMANENT since they will be expired via cache
247     // tags locally. Because of this, page cache ignores max age.
248     // - Get the tags from CacheableResponseInterface per the earlier comments.
249     // - Get the time expiration from the Expires header, rather than the
250     //   interface, but see https://www.drupal.org/node/2352009 about possibly
251     //   changing that.
252     $expire = 0;
253     // 403 and 404 responses can fill non-LRU cache backends and generally are
254     // likely to have a low cache hit rate. So do not cache them permanently.
255     if ($response->isClientError()) {
256       // Cache for an hour by default. If the 'cache_ttl_4xx' setting is
257       // set to 0 then do not cache the response.
258       $cache_ttl_4xx = Settings::get('cache_ttl_4xx', 3600);
259       if ($cache_ttl_4xx > 0) {
260         $expire = $request_time + $cache_ttl_4xx;
261       }
262     }
263     // The getExpires method could return NULL if Expires header is not set, so
264     // the returned value needs to be checked before calling getTimestamp.
265     elseif ($expires = $response->getExpires()) {
266       $date = $expires->getTimestamp();
267       $expire = ($date > $request_time) ? $date : Cache::PERMANENT;
268     }
269     else {
270       $expire = Cache::PERMANENT;
271     }
272
273     if ($expire === Cache::PERMANENT || $expire > $request_time) {
274       $tags = $response->getCacheableMetadata()->getCacheTags();
275       $this->set($request, $response, $expire, $tags);
276     }
277
278     return TRUE;
279   }
280
281   /**
282    * Returns a response object from the page cache.
283    *
284    * @param \Symfony\Component\HttpFoundation\Request $request
285    *   A request object.
286    * @param bool $allow_invalid
287    *   (optional) If TRUE, a cache item may be returned even if it is expired or
288    *   has been invalidated. Such items may sometimes be preferred, if the
289    *   alternative is recalculating the value stored in the cache, especially
290    *   if another concurrent request is already recalculating the same value.
291    *   The "valid" property of the returned object indicates whether the item is
292    *   valid or not. Defaults to FALSE.
293    *
294    * @return \Symfony\Component\HttpFoundation\Response|false
295    *   The cached response or FALSE on failure.
296    */
297   protected function get(Request $request, $allow_invalid = FALSE) {
298     $cid = $this->getCacheId($request);
299     if ($cache = $this->cache->get($cid, $allow_invalid)) {
300       return $cache->data;
301     }
302     return FALSE;
303   }
304
305   /**
306    * Stores a response object in the page cache.
307    *
308    * @param \Symfony\Component\HttpFoundation\Request $request
309    *   A request object.
310    * @param \Symfony\Component\HttpFoundation\Response $response
311    *   The response to store in the cache.
312    * @param int $expire
313    *   One of the following values:
314    *   - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should
315    *     not be removed unless it is deleted explicitly.
316    *   - A Unix timestamp: Indicates that the item will be considered invalid
317    *     after this time, i.e. it will not be returned by get() unless
318    *     $allow_invalid has been set to TRUE. When the item has expired, it may
319    *     be permanently deleted by the garbage collector at any time.
320    * @param array $tags
321    *   An array of tags to be stored with the cache item. These should normally
322    *   identify objects used to build the cache item, which should trigger
323    *   cache invalidation when updated. For example if a cached item represents
324    *   a node, both the node ID and the author's user ID might be passed in as
325    *   tags. For example array('node' => array(123), 'user' => array(92)).
326    */
327   protected function set(Request $request, Response $response, $expire, array $tags) {
328     $cid = $this->getCacheId($request);
329     $this->cache->set($cid, $response, $expire, $tags);
330   }
331
332   /**
333    * Gets the page cache ID for this request.
334    *
335    * @param \Symfony\Component\HttpFoundation\Request $request
336    *   A request object.
337    *
338    * @return string
339    *   The cache ID for this request.
340    */
341   protected function getCacheId(Request $request) {
342     $cid_parts = [
343       $request->getSchemeAndHttpHost() . $request->getRequestUri(),
344       $request->getRequestFormat(),
345     ];
346     return implode(':', $cid_parts);
347   }
348
349 }