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