Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Core\EventSubscriber;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Datetime\DateTimePlus;
|
Chris@0
|
6 use Drupal\Core\Cache\CacheableResponseInterface;
|
Chris@0
|
7 use Drupal\Core\Cache\Context\CacheContextsManager;
|
Chris@0
|
8 use Drupal\Core\Config\ConfigFactoryInterface;
|
Chris@0
|
9 use Drupal\Core\Language\LanguageManagerInterface;
|
Chris@0
|
10 use Drupal\Core\PageCache\RequestPolicyInterface;
|
Chris@0
|
11 use Drupal\Core\PageCache\ResponsePolicyInterface;
|
Chris@0
|
12 use Drupal\Core\Site\Settings;
|
Chris@0
|
13 use Symfony\Component\HttpFoundation\Request;
|
Chris@0
|
14 use Symfony\Component\HttpFoundation\Response;
|
Chris@0
|
15 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
Chris@0
|
16 use Symfony\Component\HttpKernel\KernelEvents;
|
Chris@0
|
17 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
Chris@0
|
18
|
Chris@0
|
19 /**
|
Chris@0
|
20 * Response subscriber to handle finished responses.
|
Chris@0
|
21 */
|
Chris@0
|
22 class FinishResponseSubscriber implements EventSubscriberInterface {
|
Chris@0
|
23
|
Chris@0
|
24 /**
|
Chris@0
|
25 * The language manager object for retrieving the correct language code.
|
Chris@0
|
26 *
|
Chris@0
|
27 * @var \Drupal\Core\Language\LanguageManagerInterface
|
Chris@0
|
28 */
|
Chris@0
|
29 protected $languageManager;
|
Chris@0
|
30
|
Chris@0
|
31 /**
|
Chris@0
|
32 * A config object for the system performance configuration.
|
Chris@0
|
33 *
|
Chris@0
|
34 * @var \Drupal\Core\Config\Config
|
Chris@0
|
35 */
|
Chris@0
|
36 protected $config;
|
Chris@0
|
37
|
Chris@0
|
38 /**
|
Chris@0
|
39 * A policy rule determining the cacheability of a request.
|
Chris@0
|
40 *
|
Chris@0
|
41 * @var \Drupal\Core\PageCache\RequestPolicyInterface
|
Chris@0
|
42 */
|
Chris@0
|
43 protected $requestPolicy;
|
Chris@0
|
44
|
Chris@0
|
45 /**
|
Chris@0
|
46 * A policy rule determining the cacheability of the response.
|
Chris@0
|
47 *
|
Chris@0
|
48 * @var \Drupal\Core\PageCache\ResponsePolicyInterface
|
Chris@0
|
49 */
|
Chris@0
|
50 protected $responsePolicy;
|
Chris@0
|
51
|
Chris@0
|
52 /**
|
Chris@0
|
53 * The cache contexts manager service.
|
Chris@0
|
54 *
|
Chris@0
|
55 * @var \Drupal\Core\Cache\Context\CacheContextsManager
|
Chris@0
|
56 */
|
Chris@0
|
57 protected $cacheContexts;
|
Chris@0
|
58
|
Chris@0
|
59 /**
|
Chris@0
|
60 * Whether to send cacheability headers for debugging purposes.
|
Chris@0
|
61 *
|
Chris@0
|
62 * @var bool
|
Chris@0
|
63 */
|
Chris@0
|
64 protected $debugCacheabilityHeaders = FALSE;
|
Chris@0
|
65
|
Chris@0
|
66 /**
|
Chris@0
|
67 * Constructs a FinishResponseSubscriber object.
|
Chris@0
|
68 *
|
Chris@0
|
69 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
Chris@0
|
70 * The language manager object for retrieving the correct language code.
|
Chris@0
|
71 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
Chris@0
|
72 * A config factory for retrieving required config objects.
|
Chris@0
|
73 * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
|
Chris@0
|
74 * A policy rule determining the cacheability of a request.
|
Chris@0
|
75 * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
|
Chris@0
|
76 * A policy rule determining the cacheability of a response.
|
Chris@0
|
77 * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
|
Chris@0
|
78 * The cache contexts manager service.
|
Chris@0
|
79 * @param bool $http_response_debug_cacheability_headers
|
Chris@0
|
80 * (optional) Whether to send cacheability headers for debugging purposes.
|
Chris@0
|
81 */
|
Chris@0
|
82 public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, CacheContextsManager $cache_contexts_manager, $http_response_debug_cacheability_headers = FALSE) {
|
Chris@0
|
83 $this->languageManager = $language_manager;
|
Chris@0
|
84 $this->config = $config_factory->get('system.performance');
|
Chris@0
|
85 $this->requestPolicy = $request_policy;
|
Chris@0
|
86 $this->responsePolicy = $response_policy;
|
Chris@0
|
87 $this->cacheContextsManager = $cache_contexts_manager;
|
Chris@0
|
88 $this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers;
|
Chris@0
|
89 }
|
Chris@0
|
90
|
Chris@0
|
91 /**
|
Chris@0
|
92 * Sets extra headers on any responses, also subrequest ones.
|
Chris@0
|
93 *
|
Chris@0
|
94 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
Chris@0
|
95 * The event to process.
|
Chris@0
|
96 */
|
Chris@0
|
97 public function onAllResponds(FilterResponseEvent $event) {
|
Chris@0
|
98 $response = $event->getResponse();
|
Chris@0
|
99 // Always add the 'http_response' cache tag to be able to invalidate every
|
Chris@0
|
100 // response, for example after rebuilding routes.
|
Chris@0
|
101 if ($response instanceof CacheableResponseInterface) {
|
Chris@0
|
102 $response->getCacheableMetadata()->addCacheTags(['http_response']);
|
Chris@0
|
103 }
|
Chris@0
|
104 }
|
Chris@0
|
105
|
Chris@0
|
106 /**
|
Chris@0
|
107 * Sets extra headers on successful responses.
|
Chris@0
|
108 *
|
Chris@0
|
109 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
|
Chris@0
|
110 * The event to process.
|
Chris@0
|
111 */
|
Chris@0
|
112 public function onRespond(FilterResponseEvent $event) {
|
Chris@0
|
113 if (!$event->isMasterRequest()) {
|
Chris@0
|
114 return;
|
Chris@0
|
115 }
|
Chris@0
|
116
|
Chris@0
|
117 $request = $event->getRequest();
|
Chris@0
|
118 $response = $event->getResponse();
|
Chris@0
|
119
|
Chris@0
|
120 // Set the X-UA-Compatible HTTP header to force IE to use the most recent
|
Chris@0
|
121 // rendering engine.
|
Chris@0
|
122 $response->headers->set('X-UA-Compatible', 'IE=edge', FALSE);
|
Chris@0
|
123
|
Chris@0
|
124 // Set the Content-language header.
|
Chris@0
|
125 $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->getId());
|
Chris@0
|
126
|
Chris@0
|
127 // Prevent browsers from sniffing a response and picking a MIME type
|
Chris@0
|
128 // different from the declared content-type, since that can lead to
|
Chris@0
|
129 // XSS and other vulnerabilities.
|
Chris@0
|
130 // https://www.owasp.org/index.php/List_of_useful_HTTP_headers
|
Chris@0
|
131 $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE);
|
Chris@0
|
132 $response->headers->set('X-Frame-Options', 'SAMEORIGIN', FALSE);
|
Chris@0
|
133
|
Chris@0
|
134 // If the current response isn't an implementation of the
|
Chris@0
|
135 // CacheableResponseInterface, we assume that a Response is either
|
Chris@0
|
136 // explicitly not cacheable or that caching headers are already set in
|
Chris@0
|
137 // another place.
|
Chris@0
|
138 if (!$response instanceof CacheableResponseInterface) {
|
Chris@0
|
139 if (!$this->isCacheControlCustomized($response)) {
|
Chris@0
|
140 $this->setResponseNotCacheable($response, $request);
|
Chris@0
|
141 }
|
Chris@0
|
142
|
Chris@0
|
143 // HTTP/1.0 proxies do not support the Vary header, so prevent any caching
|
Chris@0
|
144 // by sending an Expires date in the past. HTTP/1.1 clients ignore the
|
Chris@0
|
145 // Expires header if a Cache-Control: max-age directive is specified (see
|
Chris@0
|
146 // RFC 2616, section 14.9.3).
|
Chris@0
|
147 if (!$response->headers->has('Expires')) {
|
Chris@0
|
148 $this->setExpiresNoCache($response);
|
Chris@0
|
149 }
|
Chris@0
|
150 return;
|
Chris@0
|
151 }
|
Chris@0
|
152
|
Chris@0
|
153 if ($this->debugCacheabilityHeaders) {
|
Chris@0
|
154 // Expose the cache contexts and cache tags associated with this page in a
|
Chris@0
|
155 // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively.
|
Chris@0
|
156 $response_cacheability = $response->getCacheableMetadata();
|
Chris@0
|
157 $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags()));
|
Chris@0
|
158 $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts())));
|
Chris@0
|
159 }
|
Chris@0
|
160
|
Chris@0
|
161 $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY);
|
Chris@0
|
162
|
Chris@0
|
163 // Add headers necessary to specify whether the response should be cached by
|
Chris@0
|
164 // proxies and/or the browser.
|
Chris@0
|
165 if ($is_cacheable && $this->config->get('cache.page.max_age') > 0) {
|
Chris@0
|
166 if (!$this->isCacheControlCustomized($response)) {
|
Chris@0
|
167 // Only add the default Cache-Control header if the controller did not
|
Chris@0
|
168 // specify one on the response.
|
Chris@0
|
169 $this->setResponseCacheable($response, $request);
|
Chris@0
|
170 }
|
Chris@0
|
171 }
|
Chris@0
|
172 else {
|
Chris@0
|
173 // If either the policy forbids caching or the sites configuration does
|
Chris@0
|
174 // not allow to add a max-age directive, then enforce a Cache-Control
|
Chris@0
|
175 // header declaring the response as not cacheable.
|
Chris@0
|
176 $this->setResponseNotCacheable($response, $request);
|
Chris@0
|
177 }
|
Chris@0
|
178 }
|
Chris@0
|
179
|
Chris@0
|
180 /**
|
Chris@0
|
181 * Determine whether the given response has a custom Cache-Control header.
|
Chris@0
|
182 *
|
Chris@0
|
183 * Upon construction, the ResponseHeaderBag is initialized with an empty
|
Chris@0
|
184 * Cache-Control header. Consequently it is not possible to check whether the
|
Chris@0
|
185 * header was set explicitly by simply checking its presence. Instead, it is
|
Chris@0
|
186 * necessary to examine the computed Cache-Control header and compare with
|
Chris@0
|
187 * values known to be present only when Cache-Control was never set
|
Chris@0
|
188 * explicitly.
|
Chris@0
|
189 *
|
Chris@0
|
190 * When neither Cache-Control nor any of the ETag, Last-Modified, Expires
|
Chris@0
|
191 * headers are set on the response, ::get('Cache-Control') returns the value
|
Chris@0
|
192 * 'no-cache, private'. If any of ETag, Last-Modified or Expires are set but
|
Chris@0
|
193 * not Cache-Control, then 'private, must-revalidate' (in exactly this order)
|
Chris@0
|
194 * is returned.
|
Chris@0
|
195 *
|
Chris@0
|
196 * @see \Symfony\Component\HttpFoundation\ResponseHeaderBag::computeCacheControlValue()
|
Chris@0
|
197 *
|
Chris@0
|
198 * @param \Symfony\Component\HttpFoundation\Response $response
|
Chris@0
|
199 *
|
Chris@0
|
200 * @return bool
|
Chris@0
|
201 * TRUE when Cache-Control header was set explicitly on the given response.
|
Chris@0
|
202 */
|
Chris@0
|
203 protected function isCacheControlCustomized(Response $response) {
|
Chris@0
|
204 $cache_control = $response->headers->get('Cache-Control');
|
Chris@0
|
205 return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate';
|
Chris@0
|
206 }
|
Chris@0
|
207
|
Chris@0
|
208 /**
|
Chris@0
|
209 * Add Cache-Control and Expires headers to a response which is not cacheable.
|
Chris@0
|
210 *
|
Chris@0
|
211 * @param \Symfony\Component\HttpFoundation\Response $response
|
Chris@0
|
212 * A response object.
|
Chris@0
|
213 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@0
|
214 * A request object.
|
Chris@0
|
215 */
|
Chris@0
|
216 protected function setResponseNotCacheable(Response $response, Request $request) {
|
Chris@0
|
217 $this->setCacheControlNoCache($response);
|
Chris@0
|
218 $this->setExpiresNoCache($response);
|
Chris@0
|
219
|
Chris@0
|
220 // There is no point in sending along headers necessary for cache
|
Chris@0
|
221 // revalidation, if caching by proxies and browsers is denied in the first
|
Chris@0
|
222 // place. Therefore remove ETag, Last-Modified and Vary in that case.
|
Chris@0
|
223 $response->setEtag(NULL);
|
Chris@0
|
224 $response->setLastModified(NULL);
|
Chris@0
|
225 $response->setVary(NULL);
|
Chris@0
|
226 }
|
Chris@0
|
227
|
Chris@0
|
228 /**
|
Chris@0
|
229 * Add Cache-Control and Expires headers to a cacheable response.
|
Chris@0
|
230 *
|
Chris@0
|
231 * @param \Symfony\Component\HttpFoundation\Response $response
|
Chris@0
|
232 * A response object.
|
Chris@0
|
233 * @param \Symfony\Component\HttpFoundation\Request $request
|
Chris@0
|
234 * A request object.
|
Chris@0
|
235 */
|
Chris@0
|
236 protected function setResponseCacheable(Response $response, Request $request) {
|
Chris@0
|
237 // HTTP/1.0 proxies do not support the Vary header, so prevent any caching
|
Chris@0
|
238 // by sending an Expires date in the past. HTTP/1.1 clients ignore the
|
Chris@0
|
239 // Expires header if a Cache-Control: max-age directive is specified (see
|
Chris@0
|
240 // RFC 2616, section 14.9.3).
|
Chris@0
|
241 if (!$response->headers->has('Expires')) {
|
Chris@0
|
242 $this->setExpiresNoCache($response);
|
Chris@0
|
243 }
|
Chris@0
|
244
|
Chris@0
|
245 $max_age = $this->config->get('cache.page.max_age');
|
Chris@0
|
246 $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
|
Chris@0
|
247
|
Chris@0
|
248 // In order to support HTTP cache-revalidation, ensure that there is a
|
Chris@0
|
249 // Last-Modified and an ETag header on the response.
|
Chris@0
|
250 if (!$response->headers->has('Last-Modified')) {
|
Chris@0
|
251 $timestamp = REQUEST_TIME;
|
Chris@0
|
252 $response->setLastModified(new \DateTime(gmdate(DateTimePlus::RFC7231, REQUEST_TIME)));
|
Chris@0
|
253 }
|
Chris@0
|
254 else {
|
Chris@0
|
255 $timestamp = $response->getLastModified()->getTimestamp();
|
Chris@0
|
256 }
|
Chris@0
|
257 $response->setEtag($timestamp);
|
Chris@0
|
258
|
Chris@0
|
259 // Allow HTTP proxies to cache pages for anonymous users without a session
|
Chris@0
|
260 // cookie. The Vary header is used to indicates the set of request-header
|
Chris@0
|
261 // fields that fully determines whether a cache is permitted to use the
|
Chris@0
|
262 // response to reply to a subsequent request for a given URL without
|
Chris@0
|
263 // revalidation.
|
Chris@0
|
264 if (!$response->hasVary() && !Settings::get('omit_vary_cookie')) {
|
Chris@0
|
265 $response->setVary('Cookie', FALSE);
|
Chris@0
|
266 }
|
Chris@0
|
267 }
|
Chris@0
|
268
|
Chris@0
|
269 /**
|
Chris@0
|
270 * Disable caching in the browser and for HTTP/1.1 proxies and clients.
|
Chris@0
|
271 *
|
Chris@0
|
272 * @param \Symfony\Component\HttpFoundation\Response $response
|
Chris@0
|
273 * A response object.
|
Chris@0
|
274 */
|
Chris@0
|
275 protected function setCacheControlNoCache(Response $response) {
|
Chris@0
|
276 $response->headers->set('Cache-Control', 'no-cache, must-revalidate');
|
Chris@0
|
277 }
|
Chris@0
|
278
|
Chris@0
|
279 /**
|
Chris@0
|
280 * Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
|
Chris@0
|
281 *
|
Chris@0
|
282 * HTTP/1.0 proxies do not support the Vary header, so prevent any caching by
|
Chris@0
|
283 * sending an Expires date in the past. HTTP/1.1 clients ignore the Expires
|
Chris@0
|
284 * header if a Cache-Control: max-age= directive is specified (see RFC 2616,
|
Chris@0
|
285 * section 14.9.3).
|
Chris@0
|
286 *
|
Chris@0
|
287 * @param \Symfony\Component\HttpFoundation\Response $response
|
Chris@0
|
288 * A response object.
|
Chris@0
|
289 */
|
Chris@0
|
290 protected function setExpiresNoCache(Response $response) {
|
Chris@0
|
291 $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 UTC'));
|
Chris@0
|
292 }
|
Chris@0
|
293
|
Chris@0
|
294 /**
|
Chris@0
|
295 * Registers the methods in this class that should be listeners.
|
Chris@0
|
296 *
|
Chris@0
|
297 * @return array
|
Chris@0
|
298 * An array of event listener definitions.
|
Chris@0
|
299 */
|
Chris@0
|
300 public static function getSubscribedEvents() {
|
Chris@0
|
301 $events[KernelEvents::RESPONSE][] = ['onRespond'];
|
Chris@0
|
302 // There is no specific reason for choosing 16 beside it should be executed
|
Chris@0
|
303 // before ::onRespond().
|
Chris@0
|
304 $events[KernelEvents::RESPONSE][] = ['onAllResponds', 16];
|
Chris@0
|
305 return $events;
|
Chris@0
|
306 }
|
Chris@0
|
307
|
Chris@0
|
308 }
|