Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 87 |
| PageCache | |
0.00% |
0 / 1 |
|
0.00% |
0 / 8 |
1056 | |
0.00% |
0 / 87 |
| __construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
| handle | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 8 |
|||
| pass | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
| lookup | |
0.00% |
0 / 1 |
306 | |
0.00% |
0 / 40 |
|||
| fetch | |
0.00% |
0 / 1 |
42 | |
0.00% |
0 / 17 |
|||
| get | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 6 |
|||
| set | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
| getCacheId | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\page_cache\StackMiddleware\PageCache. | |
| */ | |
| namespace Drupal\page_cache\StackMiddleware; | |
| use Drupal\Core\Cache\Cache; | |
| use Drupal\Core\Cache\CacheableResponseInterface; | |
| use Drupal\Core\Cache\CacheBackendInterface; | |
| use Drupal\Core\PageCache\RequestPolicyInterface; | |
| use Drupal\Core\PageCache\ResponsePolicyInterface; | |
| use Symfony\Component\HttpFoundation\BinaryFileResponse; | |
| use Symfony\Component\HttpFoundation\Request; | |
| use Symfony\Component\HttpFoundation\Response; | |
| use Symfony\Component\HttpFoundation\StreamedResponse; | |
| use Symfony\Component\HttpKernel\HttpKernelInterface; | |
| /** | |
| * Executes the page caching before the main kernel takes over the request. | |
| */ | |
| class PageCache implements HttpKernelInterface { | |
| /** | |
| * The wrapped HTTP kernel. | |
| * | |
| * @var \Symfony\Component\HttpKernel\HttpKernelInterface | |
| */ | |
| protected $httpKernel; | |
| /** | |
| * The cache bin. | |
| * | |
| * @var \Drupal\Core\Cache\CacheBackendInterface. | |
| */ | |
| protected $cache; | |
| /** | |
| * 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; | |
| /** | |
| * Constructs a PageCache object. | |
| * | |
| * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel | |
| * The decorated kernel. | |
| * @param \Drupal\Core\Cache\CacheBackendInterface $cache | |
| * The cache bin. | |
| * @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 the response. | |
| */ | |
| public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) { | |
| $this->httpKernel = $http_kernel; | |
| $this->cache = $cache; | |
| $this->requestPolicy = $request_policy; | |
| $this->responsePolicy = $response_policy; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { | |
| // Only allow page caching on master request. | |
| if ($type === static::MASTER_REQUEST && $this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) { | |
| $response = $this->lookup($request, $type, $catch); | |
| } | |
| else { | |
| $response = $this->pass($request, $type, $catch); | |
| } | |
| return $response; | |
| } | |
| /** | |
| * Sidesteps the page cache and directly forwards a request to the backend. | |
| * | |
| * @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 pass(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { | |
| return $this->httpKernel->handle($request, $type, $catch); | |
| } | |
| /** | |
| * Retrieves a response from the cache or fetches it from the backend. | |
| * | |
| * @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 lookup(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { | |
| if ($response = $this->get($request)) { | |
| $response->headers->set('X-Drupal-Cache', 'HIT'); | |
| } | |
| else { | |
| $response = $this->fetch($request, $type, $catch); | |
| } | |
| // Only allow caching in the browser and prevent that the response is stored | |
| // by an external proxy server when the following conditions apply: | |
| // 1. There is a session cookie on the request. | |
| // 2. The Vary: Cookie header is on the response. | |
| // 3. The Cache-Control header does not contain the no-cache directive. | |
| if ($request->cookies->has(session_name()) && | |
| in_array('Cookie', $response->getVary()) && | |
| !$response->headers->hasCacheControlDirective('no-cache')) { | |
| $response->setPrivate(); | |
| } | |
| // Negotiate whether to use compression. | |
| if (extension_loaded('zlib') && $response->headers->get('Content-Encoding') === 'gzip') { | |
| if (strpos($request->headers->get('Accept-Encoding'), 'gzip') !== FALSE) { | |
| // The response content is already gzip'ed, so make sure | |
| // zlib.output_compression does not compress it once more. | |
| ini_set('zlib.output_compression', '0'); | |
| } | |
| else { | |
| // The client does not support compression. Decompress the content and | |
| // remove the Content-Encoding header. | |
| $content = $response->getContent(); | |
| $content = gzinflate(substr(substr($content, 10), 0, -8)); | |
| $response->setContent($content); | |
| $response->headers->remove('Content-Encoding'); | |
| } | |
| } | |
| // Perform HTTP revalidation. | |
| // @todo Use Response::isNotModified() as | |
| // per https://www.drupal.org/node/2259489. | |
| $last_modified = $response->getLastModified(); | |
| if ($last_modified) { | |
| // See if the client has provided the required HTTP headers. | |
| $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE; | |
| $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE; | |
| if ($if_modified_since && $if_none_match | |
| && $if_none_match == $response->getEtag() // etag must match | |
| && $if_modified_since == $last_modified->getTimestamp()) { // if-modified-since must match | |
| $response->setStatusCode(304); | |
| $response->setContent(NULL); | |
| // In the case of a 304 response, certain headers must be sent, and the | |
| // remaining may not (see RFC 2616, section 10.3.5). | |
| foreach (array_keys($response->headers->all()) as $name) { | |
| if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) { | |
| $response->headers->remove($name); | |
| } | |
| } | |
| } | |
| } | |
| return $response; | |
| } | |
| /** | |
| * 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; | |
| } | |
| /** | |
| * Returns a response object from the page cache. | |
| * | |
| * @param \Symfony\Component\HttpFoundation\Request $request | |
| * A request object. | |
| * @param bool $allow_invalid | |
| * (optional) If TRUE, a cache item may be returned even if it is expired or | |
| * has been invalidated. Such items may sometimes be preferred, if the | |
| * alternative is recalculating the value stored in the cache, especially | |
| * if another concurrent request is already recalculating the same value. | |
| * The "valid" property of the returned object indicates whether the item is | |
| * valid or not. Defaults to FALSE. | |
| * | |
| * @return \Symfony\Component\HttpFoundation\Response|false | |
| * The cached response or FALSE on failure. | |
| */ | |
| protected function get(Request $request, $allow_invalid = FALSE) { | |
| $cid = $this->getCacheId($request); | |
| if ($cache = $this->cache->get($cid, $allow_invalid)) { | |
| return $cache->data; | |
| } | |
| return FALSE; | |
| } | |
| /** | |
| * Stores a response object in the page cache. | |
| * | |
| * @param \Symfony\Component\HttpFoundation\Request $request | |
| * A request object. | |
| * @param \Symfony\Component\HttpFoundation\Response $response | |
| * The response to store in the cache. | |
| * @param int $expire | |
| * One of the following values: | |
| * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should | |
| * not be removed unless it is deleted explicitly. | |
| * - A Unix timestamp: Indicates that the item will be considered invalid | |
| * after this time, i.e. it will not be returned by get() unless | |
| * $allow_invalid has been set to TRUE. When the item has expired, it may | |
| * be permanently deleted by the garbage collector at any time. | |
| * @param array $tags | |
| * An array of tags to be stored with the cache item. These should normally | |
| * identify objects used to build the cache item, which should trigger | |
| * cache invalidation when updated. For example if a cached item represents | |
| * a node, both the node ID and the author's user ID might be passed in as | |
| * tags. For example array('node' => array(123), 'user' => array(92)). | |
| */ | |
| protected function set(Request $request, Response $response, $expire, array $tags) { | |
| $cid = $this->getCacheId($request); | |
| $this->cache->set($cid, $response, $expire, $tags); | |
| } | |
| /** | |
| * Gets the page cache ID for this request. | |
| * | |
| * @param \Symfony\Component\HttpFoundation\Request $request | |
| * A request object. | |
| * | |
| * @return string | |
| * The cache ID for this request. | |
| */ | |
| protected function getCacheId(Request $request) { | |
| $cid_parts = array( | |
| $request->getUri(), | |
| $request->getRequestFormat(), | |
| ); | |
| return implode(':', $cid_parts); | |
| } | |
| } |