FinishResponseSubscriber.php 12 KB

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