annotate core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
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 }