Mercurial > hg > isophonics-drupal-site
comparison core/modules/page_cache/src/StackMiddleware/PageCache.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 129ea1e6d783 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 } |