Mercurial > hg > isophonics-drupal-site
diff core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php Wed Nov 29 16:09:58 2017 +0000 @@ -0,0 +1,308 @@ +<?php + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Component\Datetime\DateTimePlus; +use Drupal\Core\Cache\CacheableResponseInterface; +use Drupal\Core\Cache\Context\CacheContextsManager; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\PageCache\RequestPolicyInterface; +use Drupal\Core\PageCache\ResponsePolicyInterface; +use Drupal\Core\Site\Settings; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Response subscriber to handle finished responses. + */ +class FinishResponseSubscriber implements EventSubscriberInterface { + + /** + * The language manager object for retrieving the correct language code. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * A config object for the system performance configuration. + * + * @var \Drupal\Core\Config\Config + */ + protected $config; + + /** + * A policy rule determining the cacheability of a request. + * + * @var \Drupal\Core\PageCache\RequestPolicyInterface + */ + protected $requestPolicy; + + /** + * A policy rule determining the cacheability of the response. + * + * @var \Drupal\Core\PageCache\ResponsePolicyInterface + */ + protected $responsePolicy; + + /** + * The cache contexts manager service. + * + * @var \Drupal\Core\Cache\Context\CacheContextsManager + */ + protected $cacheContexts; + + /** + * Whether to send cacheability headers for debugging purposes. + * + * @var bool + */ + protected $debugCacheabilityHeaders = FALSE; + + /** + * Constructs a FinishResponseSubscriber object. + * + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager object for retrieving the correct language code. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * A config factory for retrieving required config objects. + * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy + * A policy rule determining the cacheability of a request. + * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy + * A policy rule determining the cacheability of a response. + * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager + * The cache contexts manager service. + * @param bool $http_response_debug_cacheability_headers + * (optional) Whether to send cacheability headers for debugging purposes. + */ + 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) { + $this->languageManager = $language_manager; + $this->config = $config_factory->get('system.performance'); + $this->requestPolicy = $request_policy; + $this->responsePolicy = $response_policy; + $this->cacheContextsManager = $cache_contexts_manager; + $this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers; + } + + /** + * Sets extra headers on any responses, also subrequest ones. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onAllResponds(FilterResponseEvent $event) { + $response = $event->getResponse(); + // Always add the 'http_response' cache tag to be able to invalidate every + // response, for example after rebuilding routes. + if ($response instanceof CacheableResponseInterface) { + $response->getCacheableMetadata()->addCacheTags(['http_response']); + } + } + + /** + * Sets extra headers on successful responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $request = $event->getRequest(); + $response = $event->getResponse(); + + // Set the X-UA-Compatible HTTP header to force IE to use the most recent + // rendering engine. + $response->headers->set('X-UA-Compatible', 'IE=edge', FALSE); + + // Set the Content-language header. + $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->getId()); + + // Prevent browsers from sniffing a response and picking a MIME type + // different from the declared content-type, since that can lead to + // XSS and other vulnerabilities. + // https://www.owasp.org/index.php/List_of_useful_HTTP_headers + $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE); + $response->headers->set('X-Frame-Options', 'SAMEORIGIN', FALSE); + + // If the current response isn't an implementation of the + // CacheableResponseInterface, we assume that a Response is either + // explicitly not cacheable or that caching headers are already set in + // another place. + if (!$response instanceof CacheableResponseInterface) { + if (!$this->isCacheControlCustomized($response)) { + $this->setResponseNotCacheable($response, $request); + } + + // HTTP/1.0 proxies do not support the Vary header, so prevent any caching + // by sending an Expires date in the past. HTTP/1.1 clients ignore the + // Expires header if a Cache-Control: max-age directive is specified (see + // RFC 2616, section 14.9.3). + if (!$response->headers->has('Expires')) { + $this->setExpiresNoCache($response); + } + return; + } + + if ($this->debugCacheabilityHeaders) { + // Expose the cache contexts and cache tags associated with this page in a + // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. + $response_cacheability = $response->getCacheableMetadata(); + $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags())); + $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts()))); + } + + $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY); + + // Add headers necessary to specify whether the response should be cached by + // proxies and/or the browser. + if ($is_cacheable && $this->config->get('cache.page.max_age') > 0) { + if (!$this->isCacheControlCustomized($response)) { + // Only add the default Cache-Control header if the controller did not + // specify one on the response. + $this->setResponseCacheable($response, $request); + } + } + else { + // If either the policy forbids caching or the sites configuration does + // not allow to add a max-age directive, then enforce a Cache-Control + // header declaring the response as not cacheable. + $this->setResponseNotCacheable($response, $request); + } + } + + /** + * Determine whether the given response has a custom Cache-Control header. + * + * Upon construction, the ResponseHeaderBag is initialized with an empty + * Cache-Control header. Consequently it is not possible to check whether the + * header was set explicitly by simply checking its presence. Instead, it is + * necessary to examine the computed Cache-Control header and compare with + * values known to be present only when Cache-Control was never set + * explicitly. + * + * When neither Cache-Control nor any of the ETag, Last-Modified, Expires + * headers are set on the response, ::get('Cache-Control') returns the value + * 'no-cache, private'. If any of ETag, Last-Modified or Expires are set but + * not Cache-Control, then 'private, must-revalidate' (in exactly this order) + * is returned. + * + * @see \Symfony\Component\HttpFoundation\ResponseHeaderBag::computeCacheControlValue() + * + * @param \Symfony\Component\HttpFoundation\Response $response + * + * @return bool + * TRUE when Cache-Control header was set explicitly on the given response. + */ + protected function isCacheControlCustomized(Response $response) { + $cache_control = $response->headers->get('Cache-Control'); + return $cache_control != 'no-cache, private' && $cache_control != 'private, must-revalidate'; + } + + /** + * Add Cache-Control and Expires headers to a response which is not cacheable. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * A response object. + * @param \Symfony\Component\HttpFoundation\Request $request + * A request object. + */ + protected function setResponseNotCacheable(Response $response, Request $request) { + $this->setCacheControlNoCache($response); + $this->setExpiresNoCache($response); + + // There is no point in sending along headers necessary for cache + // revalidation, if caching by proxies and browsers is denied in the first + // place. Therefore remove ETag, Last-Modified and Vary in that case. + $response->setEtag(NULL); + $response->setLastModified(NULL); + $response->setVary(NULL); + } + + /** + * Add Cache-Control and Expires headers to a cacheable response. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * A response object. + * @param \Symfony\Component\HttpFoundation\Request $request + * A request object. + */ + protected function setResponseCacheable(Response $response, Request $request) { + // HTTP/1.0 proxies do not support the Vary header, so prevent any caching + // by sending an Expires date in the past. HTTP/1.1 clients ignore the + // Expires header if a Cache-Control: max-age directive is specified (see + // RFC 2616, section 14.9.3). + if (!$response->headers->has('Expires')) { + $this->setExpiresNoCache($response); + } + + $max_age = $this->config->get('cache.page.max_age'); + $response->headers->set('Cache-Control', 'public, max-age=' . $max_age); + + // In order to support HTTP cache-revalidation, ensure that there is a + // Last-Modified and an ETag header on the response. + if (!$response->headers->has('Last-Modified')) { + $timestamp = REQUEST_TIME; + $response->setLastModified(new \DateTime(gmdate(DateTimePlus::RFC7231, REQUEST_TIME))); + } + else { + $timestamp = $response->getLastModified()->getTimestamp(); + } + $response->setEtag($timestamp); + + // Allow HTTP proxies to cache pages for anonymous users without a session + // cookie. The Vary header is used to indicates the set of request-header + // fields that fully determines whether a cache is permitted to use the + // response to reply to a subsequent request for a given URL without + // revalidation. + if (!$response->hasVary() && !Settings::get('omit_vary_cookie')) { + $response->setVary('Cookie', FALSE); + } + } + + /** + * Disable caching in the browser and for HTTP/1.1 proxies and clients. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * A response object. + */ + protected function setCacheControlNoCache(Response $response) { + $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); + } + + /** + * Disable caching in ancient browsers and for HTTP/1.0 proxies and clients. + * + * HTTP/1.0 proxies do not support the Vary header, so prevent any caching by + * sending an Expires date in the past. HTTP/1.1 clients ignore the Expires + * header if a Cache-Control: max-age= directive is specified (see RFC 2616, + * section 14.9.3). + * + * @param \Symfony\Component\HttpFoundation\Response $response + * A response object. + */ + protected function setExpiresNoCache(Response $response) { + $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 UTC')); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + public static function getSubscribedEvents() { + $events[KernelEvents::RESPONSE][] = ['onRespond']; + // There is no specific reason for choosing 16 beside it should be executed + // before ::onRespond(). + $events[KernelEvents::RESPONSE][] = ['onAllResponds', 16]; + return $events; + } + +}