Chris@0: httpKernel = $http_kernel; Chris@0: $this->cache = $cache; Chris@0: $this->requestPolicy = $request_policy; Chris@0: $this->responsePolicy = $response_policy; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { Chris@0: // Only allow page caching on master request. Chris@0: if ($type === static::MASTER_REQUEST && $this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) { Chris@0: $response = $this->lookup($request, $type, $catch); Chris@0: } Chris@0: else { Chris@0: $response = $this->pass($request, $type, $catch); Chris@0: } Chris@0: Chris@0: return $response; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sidesteps the page cache and directly forwards a request to the backend. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param int $type Chris@0: * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or Chris@0: * HttpKernelInterface::SUB_REQUEST) Chris@0: * @param bool $catch Chris@0: * Whether to catch exceptions or not Chris@0: * Chris@0: * @returns \Symfony\Component\HttpFoundation\Response $response Chris@0: * A response object. Chris@0: */ Chris@0: protected function pass(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { Chris@0: return $this->httpKernel->handle($request, $type, $catch); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves a response from the cache or fetches it from the backend. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param int $type Chris@0: * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or Chris@0: * HttpKernelInterface::SUB_REQUEST) Chris@0: * @param bool $catch Chris@0: * Whether to catch exceptions or not Chris@0: * Chris@0: * @returns \Symfony\Component\HttpFoundation\Response $response Chris@0: * A response object. Chris@0: */ Chris@0: protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { Chris@0: if ($response = $this->get($request)) { Chris@0: $response->headers->set('X-Drupal-Cache', 'HIT'); Chris@0: } Chris@0: else { Chris@0: $response = $this->fetch($request, $type, $catch); Chris@0: } Chris@0: Chris@0: // Only allow caching in the browser and prevent that the response is stored Chris@0: // by an external proxy server when the following conditions apply: Chris@0: // 1. There is a session cookie on the request. Chris@0: // 2. The Vary: Cookie header is on the response. Chris@0: // 3. The Cache-Control header does not contain the no-cache directive. Chris@0: if ($request->cookies->has(session_name()) && Chris@0: in_array('Cookie', $response->getVary()) && Chris@0: !$response->headers->hasCacheControlDirective('no-cache')) { Chris@0: Chris@0: $response->setPrivate(); Chris@0: } Chris@0: Chris@0: // Perform HTTP revalidation. Chris@0: // @todo Use Response::isNotModified() as Chris@0: // per https://www.drupal.org/node/2259489. Chris@0: $last_modified = $response->getLastModified(); Chris@0: if ($last_modified) { Chris@0: // See if the client has provided the required HTTP headers. Chris@0: $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE; Chris@0: $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE; Chris@0: Chris@0: if ($if_modified_since && $if_none_match Chris@0: // etag must match. Chris@0: && $if_none_match == $response->getEtag() Chris@0: // if-modified-since must match. Chris@0: && $if_modified_since == $last_modified->getTimestamp()) { Chris@0: $response->setStatusCode(304); Chris@0: $response->setContent(NULL); Chris@0: Chris@0: // In the case of a 304 response, certain headers must be sent, and the Chris@0: // remaining may not (see RFC 2616, section 10.3.5). Chris@0: foreach (array_keys($response->headers->all()) as $name) { Chris@0: if (!in_array($name, ['content-location', 'expires', 'cache-control', 'vary'])) { Chris@0: $response->headers->remove($name); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return $response; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Fetches a response from the backend and stores it in the cache. Chris@0: * Chris@0: * @see drupal_page_header() Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param int $type Chris@0: * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or Chris@0: * HttpKernelInterface::SUB_REQUEST) Chris@0: * @param bool $catch Chris@0: * Whether to catch exceptions or not Chris@0: * Chris@0: * @returns \Symfony\Component\HttpFoundation\Response $response Chris@0: * A response object. Chris@0: */ Chris@0: protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { Chris@0: /** @var \Symfony\Component\HttpFoundation\Response $response */ Chris@0: $response = $this->httpKernel->handle($request, $type, $catch); Chris@0: Chris@0: // Only set the 'X-Drupal-Cache' header if caching is allowed for this Chris@0: // response. Chris@0: if ($this->storeResponse($request, $response)) { Chris@0: $response->headers->set('X-Drupal-Cache', 'MISS'); Chris@0: } Chris@0: Chris@0: return $response; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stores a response in the page cache. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param \Symfony\Component\HttpFoundation\Response $response Chris@0: * A response object that should be stored in the page cache. Chris@0: * Chris@0: * @returns bool Chris@0: */ Chris@0: protected function storeResponse(Request $request, Response $response) { Chris@0: // Drupal's primary cache invalidation architecture is cache tags: any Chris@0: // response that varies by a configuration value or data in a content Chris@0: // entity should have cache tags, to allow for instant cache invalidation Chris@0: // when that data is updated. However, HTTP does not standardize how to Chris@0: // encode cache tags in a response. Different CDNs implement their own Chris@0: // approaches, and configurable reverse proxies (e.g., Varnish) allow for Chris@0: // custom implementations. To keep Drupal's internal page cache simple, we Chris@0: // only cache CacheableResponseInterface responses, since those provide a Chris@0: // defined API for retrieving cache tags. For responses that do not Chris@0: // implement CacheableResponseInterface, there's no easy way to distinguish Chris@0: // responses that truly don't depend on any site data from responses that Chris@0: // contain invalidation information customized to a particular proxy or Chris@0: // CDN. Chris@0: // - Drupal modules are encouraged to use CacheableResponseInterface Chris@0: // responses where possible and to leave the encoding of that information Chris@0: // into response headers to the corresponding proxy/CDN integration Chris@0: // modules. Chris@0: // - Custom applications that wish to provide internal page cache support Chris@0: // for responses that do not implement CacheableResponseInterface may do Chris@0: // so by replacing/extending this middleware service or adding another Chris@0: // one. Chris@0: if (!$response instanceof CacheableResponseInterface) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // Currently it is not possible to cache binary file or streamed responses: Chris@0: // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678. Chris@0: // Therefore exclude them, even for subclasses that implement Chris@0: // CacheableResponseInterface. Chris@0: if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // Allow policy rules to further restrict which responses to cache. Chris@0: if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $request_time = $request->server->get('REQUEST_TIME'); Chris@0: // The response passes all of the above checks, so cache it. Page cache Chris@0: // entries default to Cache::PERMANENT since they will be expired via cache Chris@0: // tags locally. Because of this, page cache ignores max age. Chris@0: // - Get the tags from CacheableResponseInterface per the earlier comments. Chris@0: // - Get the time expiration from the Expires header, rather than the Chris@0: // interface, but see https://www.drupal.org/node/2352009 about possibly Chris@0: // changing that. Chris@0: $expire = 0; Chris@0: // 403 and 404 responses can fill non-LRU cache backends and generally are Chris@0: // likely to have a low cache hit rate. So do not cache them permanently. Chris@0: if ($response->isClientError()) { Chris@0: // Cache for an hour by default. If the 'cache_ttl_4xx' setting is Chris@0: // set to 0 then do not cache the response. Chris@0: $cache_ttl_4xx = Settings::get('cache_ttl_4xx', 3600); Chris@0: if ($cache_ttl_4xx > 0) { Chris@0: $expire = $request_time + $cache_ttl_4xx; Chris@0: } Chris@0: } Chris@0: // The getExpires method could return NULL if Expires header is not set, so Chris@0: // the returned value needs to be checked before calling getTimestamp. Chris@0: elseif ($expires = $response->getExpires()) { Chris@0: $date = $expires->getTimestamp(); Chris@0: $expire = ($date > $request_time) ? $date : Cache::PERMANENT; Chris@0: } Chris@0: else { Chris@0: $expire = Cache::PERMANENT; Chris@0: } Chris@0: Chris@0: if ($expire === Cache::PERMANENT || $expire > $request_time) { Chris@0: $tags = $response->getCacheableMetadata()->getCacheTags(); Chris@0: $this->set($request, $response, $expire, $tags); Chris@0: } Chris@0: Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a response object from the page cache. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param bool $allow_invalid Chris@0: * (optional) If TRUE, a cache item may be returned even if it is expired or Chris@0: * has been invalidated. Such items may sometimes be preferred, if the Chris@0: * alternative is recalculating the value stored in the cache, especially Chris@0: * if another concurrent request is already recalculating the same value. Chris@0: * The "valid" property of the returned object indicates whether the item is Chris@0: * valid or not. Defaults to FALSE. Chris@0: * Chris@0: * @return \Symfony\Component\HttpFoundation\Response|false Chris@0: * The cached response or FALSE on failure. Chris@0: */ Chris@0: protected function get(Request $request, $allow_invalid = FALSE) { Chris@0: $cid = $this->getCacheId($request); Chris@0: if ($cache = $this->cache->get($cid, $allow_invalid)) { Chris@0: return $cache->data; Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stores a response object in the page cache. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * @param \Symfony\Component\HttpFoundation\Response $response Chris@0: * The response to store in the cache. Chris@0: * @param int $expire Chris@0: * One of the following values: Chris@0: * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should Chris@0: * not be removed unless it is deleted explicitly. Chris@0: * - A Unix timestamp: Indicates that the item will be considered invalid Chris@0: * after this time, i.e. it will not be returned by get() unless Chris@0: * $allow_invalid has been set to TRUE. When the item has expired, it may Chris@0: * be permanently deleted by the garbage collector at any time. Chris@0: * @param array $tags Chris@0: * An array of tags to be stored with the cache item. These should normally Chris@0: * identify objects used to build the cache item, which should trigger Chris@0: * cache invalidation when updated. For example if a cached item represents Chris@0: * a node, both the node ID and the author's user ID might be passed in as Chris@0: * tags. For example array('node' => array(123), 'user' => array(92)). Chris@0: */ Chris@0: protected function set(Request $request, Response $response, $expire, array $tags) { Chris@0: $cid = $this->getCacheId($request); Chris@0: $this->cache->set($cid, $response, $expire, $tags); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the page cache ID for this request. Chris@0: * Chris@0: * @param \Symfony\Component\HttpFoundation\Request $request Chris@0: * A request object. Chris@0: * Chris@0: * @return string Chris@0: * The cache ID for this request. Chris@0: */ Chris@0: protected function getCacheId(Request $request) { Chris@18: // Once a cache ID is determined for the request, reuse it for the duration Chris@18: // of the request. This ensures that when the cache is written, it is only Chris@18: // keyed on request data that was available when it was read. For example, Chris@18: // the request format might be NULL during cache lookup and then set during Chris@18: // routing, in which case we want to key on NULL during writing, since that Chris@18: // will be the value during lookups for subsequent requests. Chris@18: if (!isset($this->cid)) { Chris@18: $cid_parts = [ Chris@18: $request->getSchemeAndHttpHost() . $request->getRequestUri(), Chris@18: $request->getRequestFormat(NULL), Chris@18: ]; Chris@18: $this->cid = implode(':', $cid_parts); Chris@18: } Chris@18: return $this->cid; Chris@0: } Chris@0: Chris@0: }