/** * Fetches a response from the backend and stores it in the cache. * * If page_compression is enabled, a gzipped version of the page is stored in * the cache to avoid compressing the output on each request. The cache entry * is unzipped in the relatively rare event that the page is requested by a * client without gzip support. * * Page compression requires the PHP zlib extension * (http://php.net/manual/ref.zlib.php). * * @see drupal_page_header() * * @param \Symfony\Component\HttpFoundation\Request $request * A request object. * @param int $type * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or * HttpKernelInterface::SUB_REQUEST) * @param bool $catch * Whether to catch exceptions or not * * @returns \Symfony\Component\HttpFoundation\Response $response * A response object. */ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { $response = $this->httpKernel->handle($request, $type, $catch); // Currently it is not possible to cache some types of responses. Therefore // exclude binary file responses (generated files, e.g. images with image // styles) and streamed responses (files directly read from the disk). // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678 if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { return $response; } if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { return $response; } // Check if the current page may be compressed. if (extension_loaded('zlib') && !$response->headers->get('Content-Encoding') && $this->config('system.performance')->get('response.gzip')) { $content = $response->getContent(); if ($content) { $response->setContent(gzencode($content, 9, FORCE_GZIP)); $response->headers->set('Content-Encoding', 'gzip'); } // When page compression is enabled, ensure that proxy caches will record // and deliver different versions of a page depending on whether the // client supports gzip or not. $response->setVary('Accept-Encoding', FALSE); } // Use the actual timestamp from an Expires header, if available. $date = $response->getExpires(); $expire = $date > new \DateTime() ? $date->getTimestamp() : Cache::PERMANENT; $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); $this->set($request, $response, $expire, $tags); // Mark response as a cache miss. $response->headers->set('X-Drupal-Cache', 'MISS'); return $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); } }
/** * 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); // Attach globally-declared headers to the response object so that Symfony // can send them for us correctly. // @todo Remove this once drupal_process_attached() no longer calls // _drupal_add_http_header(), which has its own static. Instead, // _drupal_process_attached() should use // \Symfony\Component\HttpFoundation\Response->headers->set(), which is // already documented on the (deprecated) _drupal_process_attached() to // become the final, intended mechanism. $headers = drupal_get_http_header(); foreach ($headers as $name => $value) { // Symfony special-cases the 'Status' header. if ($name === 'status') { $response->setStatusCode($value); } $response->headers->set($name, $value, FALSE); } // Expose the cache contexts and cache tags associated with this page in a // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. if ($response instanceof CacheableResponseInterface) { $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); } }
/** * Sets extra headers on successful responses. * * @param Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * The event to process. */ public function onRespond(FilterResponseEvent $event) { if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) { return; } $request = $event->getRequest(); $response = $event->getResponse(); // Set the X-UA-Compatible HTTP header to force IE to use the most recent // rendering engine or use Chrome's frame rendering engine if available. $response->headers->set('X-UA-Compatible', 'IE=edge,chrome=1', FALSE); // Set the Content-language header. $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->getId()); // Attach globally-declared headers to the response object so that Symfony // can send them for us correctly. // @todo Remove this once drupal_process_attached() no longer calls // _drupal_add_http_header(), which has its own static. Instead, // _drupal_process_attached() should use // \Symfony\Component\HttpFoundation\Response->headers->set(), which is // already documented on the (deprecated) _drupal_process_attached() to // become the final, intended mechanism. $headers = drupal_get_http_header(); foreach ($headers as $name => $value) { // Symfony special-cases the 'Status' header. if ($name === 'status') { $response->setStatusCode($value); } $response->headers->set($name, $value, FALSE); } $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); } // Currently it is not possible to cache some types of responses. Therefore // exclude binary file responses (generated files, e.g. images with image // styles) and streamed responses (files directly read from the disk). // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678 if ($is_cacheable && $this->config->get('cache.page.use_internal') && !$response instanceof BinaryFileResponse && !$response instanceof StreamedResponse) { // Store the response in the internal page cache. drupal_page_set_cache($response, $request); $response->headers->set('X-Drupal-Cache', 'MISS'); drupal_serve_page_from_cache($response, $request); } }
/** * Fetches a response from the backend and stores it in the cache. * * If page_compression is enabled, a gzipped version of the page is stored in * the cache to avoid compressing the output on each request. The cache entry * is unzipped in the relatively rare event that the page is requested by a * client without gzip support. * * Page compression requires the PHP zlib extension * (http://php.net/manual/ref.zlib.php). * * @see drupal_page_header() * * @param \Symfony\Component\HttpFoundation\Request $request * A request object. * @param int $type * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or * HttpKernelInterface::SUB_REQUEST) * @param bool $catch * Whether to catch exceptions or not * * @returns \Symfony\Component\HttpFoundation\Response $response * A response object. */ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { /** @var \Symfony\Component\HttpFoundation\Response $response */ $response = $this->httpKernel->handle($request, $type, $catch); // Drupal's primary cache invalidation architecture is cache tags: any // response that varies by a configuration value or data in a content // entity should have cache tags, to allow for instant cache invalidation // when that data is updated. However, HTTP does not standardize how to // encode cache tags in a response. Different CDNs implement their own // approaches, and configurable reverse proxies (e.g., Varnish) allow for // custom implementations. To keep Drupal's internal page cache simple, we // only cache CacheableResponseInterface responses, since those provide a // defined API for retrieving cache tags. For responses that do not // implement CacheableResponseInterface, there's no easy way to distinguish // responses that truly don't depend on any site data from responses that // contain invalidation information customized to a particular proxy or // CDN. // - Drupal modules are encouraged to use CacheableResponseInterface // responses where possible and to leave the encoding of that information // into response headers to the corresponding proxy/CDN integration // modules. // - Custom applications that wish to provide internal page cache support // for responses that do not implement CacheableResponseInterface may do // so by replacing/extending this middleware service or adding another // one. if (!$response instanceof CacheableResponseInterface) { return $response; } // Currently it is not possible to cache binary file or streamed responses: // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678. // Therefore exclude them, even for subclasses that implement // CacheableResponseInterface. if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { return $response; } // Allow policy rules to further restrict which responses to cache. if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { return $response; } // The response passes all of the above checks, so cache it. // - Get the tags from CacheableResponseInterface per the earlier comments. // - Get the time expiration from the Expires header, rather than the // interface, but see https://www.drupal.org/node/2352009 about possibly // changing that. $tags = $response->getCacheableMetadata()->getCacheTags(); $date = $response->getExpires()->getTimestamp(); $expire = $date > time() ? $date : Cache::PERMANENT; $this->set($request, $response, $expire, $tags); // Mark response as a cache miss. $response->headers->set('X-Drupal-Cache', 'MISS'); return $response; }
/** * Stores a response in case of a Dynamic Page Cache miss, if cacheable. * * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * The event to process. */ public function onResponse(FilterResponseEvent $event) { $response = $event->getResponse(); // Dynamic Page Cache only works with cacheable responses. It does not work // with plain Response objects. (Dynamic Page Cache needs to be able to // access and modify the cacheability metadata associated with the // response.) if (!$response instanceof CacheableResponseInterface) { return; } // There's no work left to be done if this is a Dynamic Page Cache hit. if ($response->headers->get(self::HEADER) === 'HIT') { return; } // There's no work left to be done if this is an uncacheable response. if (!$this->shouldCacheResponse($response)) { // The response is uncacheable, mark it as such. $response->headers->set(self::HEADER, 'UNCACHEABLE'); return; } // Don't cache the response if Dynamic Page Cache's request subscriber did // not fire, because that means it is impossible to have a Dynamic Page // Cache hit. This can happen when the master request is for example a 403 // or 404, in which case a subrequest is performed by the router. In that // case, it is the subrequest's response that is cached by Dynamic Page // Cache, because the routing happens in a request subscriber earlier than // Dynamic Page Cache's and immediately sets a response, i.e. the one // returned by the subrequest, and thus causes Dynamic Page Cache's request // subscriber to not fire for the master request. // @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess() // @see \Drupal\Core\EventSubscriber\DefaultExceptionHtmlSubscriber::on403() $request = $event->getRequest(); if (!isset($this->requestPolicyResults[$request])) { return; } // Don't cache the response if the Dynamic Page Cache request & response // policies are not met. // @see onRouteMatch() if ($this->requestPolicyResults[$request] === RequestPolicyInterface::DENY || $this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { return; } // Embed the response object in a render array so that RenderCache is able // to cache it, handling cache redirection for us. $response_as_render_array = $this->responseToRenderArray($response); $this->renderCache->set($response_as_render_array, $this->dynamicPageCacheRedirectRenderArray); // The response was generated, mark the response as a cache miss. The next // time, it will be a cache hit. $response->headers->set(self::HEADER, 'MISS'); }
/** * Fetches a response from the backend and stores it in the cache. * * If page_compression is enabled, a gzipped version of the page is stored in * the cache to avoid compressing the output on each request. The cache entry * is unzipped in the relatively rare event that the page is requested by a * client without gzip support. * * Page compression requires the PHP zlib extension * (http://php.net/manual/ref.zlib.php). * * @see drupal_page_header() * * @param \Symfony\Component\HttpFoundation\Request $request * A request object. * @param int $type * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or * HttpKernelInterface::SUB_REQUEST) * @param bool $catch * Whether to catch exceptions or not * * @returns \Symfony\Component\HttpFoundation\Response $response * A response object. */ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { $response = $this->httpKernel->handle($request, $type, $catch); // Currently it is not possible to cache some types of responses. Therefore // exclude binary file responses (generated files, e.g. images with image // styles) and streamed responses (files directly read from the disk). // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678 if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { return $response; } if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { return $response; } // Use the actual timestamp from an Expires header, if available. $date = $response->getExpires()->getTimestamp(); $expire = $date > time() ? $date : Cache::PERMANENT; $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags')); $this->set($request, $response, $expire, $tags); // Mark response as a cache miss. $response->headers->set('X-Drupal-Cache', 'MISS'); return $response; }