1. 8.2.x core/modules/page_cache/src/StackMiddleware/PageCache.php
  2. 8.0.x core/modules/page_cache/src/StackMiddleware/PageCache.php
  3. 8.1.x core/modules/page_cache/src/StackMiddleware/PageCache.php
  4. 8.3.x core/modules/page_cache/src/StackMiddleware/PageCache.php

Namespace

Drupal\page_cache\StackMiddleware

File

core/modules/page_cache/src/StackMiddleware/PageCache.php
View source
  1. <?php
  2. namespace Drupal\page_cache\StackMiddleware;
  3. use Drupal\Core\Cache\Cache;
  4. use Drupal\Core\Cache\CacheableResponseInterface;
  5. use Drupal\Core\Cache\CacheBackendInterface;
  6. use Drupal\Core\PageCache\RequestPolicyInterface;
  7. use Drupal\Core\PageCache\ResponsePolicyInterface;
  8. use Drupal\Core\Site\Settings;
  9. use Symfony\Component\HttpFoundation\BinaryFileResponse;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\Response;
  12. use Symfony\Component\HttpFoundation\StreamedResponse;
  13. use Symfony\Component\HttpKernel\HttpKernelInterface;
  14. /**
  15. * Executes the page caching before the main kernel takes over the request.
  16. */
  17. class PageCache implements HttpKernelInterface {
  18. /**
  19. * The wrapped HTTP kernel.
  20. *
  21. * @var \Symfony\Component\HttpKernel\HttpKernelInterface
  22. */
  23. protected $httpKernel;
  24. /**
  25. * The cache bin.
  26. *
  27. * @var \Drupal\Core\Cache\CacheBackendInterface.
  28. */
  29. protected $cache;
  30. /**
  31. * A policy rule determining the cacheability of a request.
  32. *
  33. * @var \Drupal\Core\PageCache\RequestPolicyInterface
  34. */
  35. protected $requestPolicy;
  36. /**
  37. * A policy rule determining the cacheability of the response.
  38. *
  39. * @var \Drupal\Core\PageCache\ResponsePolicyInterface
  40. */
  41. protected $responsePolicy;
  42. /**
  43. * Constructs a PageCache object.
  44. *
  45. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
  46. * The decorated kernel.
  47. * @param \Drupal\Core\Cache\CacheBackendInterface $cache
  48. * The cache bin.
  49. * @param \Drupal\Core\PageCache\RequestPolicyInterface $request_policy
  50. * A policy rule determining the cacheability of a request.
  51. * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
  52. * A policy rule determining the cacheability of the response.
  53. */
  54. public function __construct(HttpKernelInterface $http_kernel, CacheBackendInterface $cache, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) {
  55. $this->httpKernel = $http_kernel;
  56. $this->cache = $cache;
  57. $this->requestPolicy = $request_policy;
  58. $this->responsePolicy = $response_policy;
  59. }
  60. /**
  61. * {@inheritdoc}
  62. */
  63. public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
  64. // Only allow page caching on master request.
  65. if ($type === static::MASTER_REQUEST && $this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) {
  66. $response = $this->lookup($request, $type, $catch);
  67. }
  68. else {
  69. $response = $this->pass($request, $type, $catch);
  70. }
  71. return $response;
  72. }
  73. /**
  74. * Sidesteps the page cache and directly forwards a request to the backend.
  75. *
  76. * @param \Symfony\Component\HttpFoundation\Request $request
  77. * A request object.
  78. * @param int $type
  79. * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
  80. * HttpKernelInterface::SUB_REQUEST)
  81. * @param bool $catch
  82. * Whether to catch exceptions or not
  83. *
  84. * @returns \Symfony\Component\HttpFoundation\Response $response
  85. * A response object.
  86. */
  87. protected function pass(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
  88. return $this->httpKernel->handle($request, $type, $catch);
  89. }
  90. /**
  91. * Retrieves a response from the cache or fetches it from the backend.
  92. *
  93. * @param \Symfony\Component\HttpFoundation\Request $request
  94. * A request object.
  95. * @param int $type
  96. * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
  97. * HttpKernelInterface::SUB_REQUEST)
  98. * @param bool $catch
  99. * Whether to catch exceptions or not
  100. *
  101. * @returns \Symfony\Component\HttpFoundation\Response $response
  102. * A response object.
  103. */
  104. protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
  105. if ($response = $this->get($request)) {
  106. $response->headers->set('X-Drupal-Cache', 'HIT');
  107. }
  108. else {
  109. $response = $this->fetch($request, $type, $catch);
  110. }
  111. // Only allow caching in the browser and prevent that the response is stored
  112. // by an external proxy server when the following conditions apply:
  113. // 1. There is a session cookie on the request.
  114. // 2. The Vary: Cookie header is on the response.
  115. // 3. The Cache-Control header does not contain the no-cache directive.
  116. if ($request->cookies->has(session_name()) &&
  117. in_array('Cookie', $response->getVary()) &&
  118. !$response->headers->hasCacheControlDirective('no-cache')) {
  119. $response->setPrivate();
  120. }
  121. // Negotiate whether to use compression.
  122. if (extension_loaded('zlib') && $response->headers->get('Content-Encoding') === 'gzip') {
  123. if (strpos($request->headers->get('Accept-Encoding'), 'gzip') !== FALSE) {
  124. // The response content is already gzip'ed, so make sure
  125. // zlib.output_compression does not compress it once more.
  126. ini_set('zlib.output_compression', '0');
  127. }
  128. else {
  129. // The client does not support compression. Decompress the content and
  130. // remove the Content-Encoding header.
  131. $content = $response->getContent();
  132. $content = gzinflate(substr(substr($content, 10), 0, -8));
  133. $response->setContent($content);
  134. $response->headers->remove('Content-Encoding');
  135. }
  136. }
  137. // Perform HTTP revalidation.
  138. // @todo Use Response::isNotModified() as
  139. // per https://www.drupal.org/node/2259489.
  140. $last_modified = $response->getLastModified();
  141. if ($last_modified) {
  142. // See if the client has provided the required HTTP headers.
  143. $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
  144. $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
  145. if ($if_modified_since && $if_none_match
  146. && $if_none_match == $response->getEtag() // etag must match
  147. && $if_modified_since == $last_modified->getTimestamp()) { // if-modified-since must match
  148. $response->setStatusCode(304);
  149. $response->setContent(NULL);
  150. // In the case of a 304 response, certain headers must be sent, and the
  151. // remaining may not (see RFC 2616, section 10.3.5).
  152. foreach (array_keys($response->headers->all()) as $name) {
  153. if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) {
  154. $response->headers->remove($name);
  155. }
  156. }
  157. }
  158. }
  159. return $response;
  160. }
  161. /**
  162. * Fetches a response from the backend and stores it in the cache.
  163. *
  164. * If page_compression is enabled, a gzipped version of the page is stored in
  165. * the cache to avoid compressing the output on each request. The cache entry
  166. * is unzipped in the relatively rare event that the page is requested by a
  167. * client without gzip support.
  168. *
  169. * Page compression requires the PHP zlib extension
  170. * (http://php.net/manual/ref.zlib.php).
  171. *
  172. * @see drupal_page_header()
  173. *
  174. * @param \Symfony\Component\HttpFoundation\Request $request
  175. * A request object.
  176. * @param int $type
  177. * The type of the request (one of HttpKernelInterface::MASTER_REQUEST or
  178. * HttpKernelInterface::SUB_REQUEST)
  179. * @param bool $catch
  180. * Whether to catch exceptions or not
  181. *
  182. * @returns \Symfony\Component\HttpFoundation\Response $response
  183. * A response object.
  184. */
  185. protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
  186. /** @var \Symfony\Component\HttpFoundation\Response $response */
  187. $response = $this->httpKernel->handle($request, $type, $catch);
  188. // Drupal's primary cache invalidation architecture is cache tags: any
  189. // response that varies by a configuration value or data in a content
  190. // entity should have cache tags, to allow for instant cache invalidation
  191. // when that data is updated. However, HTTP does not standardize how to
  192. // encode cache tags in a response. Different CDNs implement their own
  193. // approaches, and configurable reverse proxies (e.g., Varnish) allow for
  194. // custom implementations. To keep Drupal's internal page cache simple, we
  195. // only cache CacheableResponseInterface responses, since those provide a
  196. // defined API for retrieving cache tags. For responses that do not
  197. // implement CacheableResponseInterface, there's no easy way to distinguish
  198. // responses that truly don't depend on any site data from responses that
  199. // contain invalidation information customized to a particular proxy or
  200. // CDN.
  201. // - Drupal modules are encouraged to use CacheableResponseInterface
  202. // responses where possible and to leave the encoding of that information
  203. // into response headers to the corresponding proxy/CDN integration
  204. // modules.
  205. // - Custom applications that wish to provide internal page cache support
  206. // for responses that do not implement CacheableResponseInterface may do
  207. // so by replacing/extending this middleware service or adding another
  208. // one.
  209. if (!$response instanceof CacheableResponseInterface) {
  210. return $response;
  211. }
  212. // Currently it is not possible to cache binary file or streamed responses:
  213. // https://github.com/symfony/symfony/issues/9128#issuecomment-25088678.
  214. // Therefore exclude them, even for subclasses that implement
  215. // CacheableResponseInterface.
  216. if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) {
  217. return $response;
  218. }
  219. // Allow policy rules to further restrict which responses to cache.
  220. if ($this->responsePolicy->check($response, $request) === ResponsePolicyInterface::DENY) {
  221. return $response;
  222. }
  223. $request_time = $request->server->get('REQUEST_TIME');
  224. // The response passes all of the above checks, so cache it. Page cache
  225. // entries default to Cache::PERMANENT since they will be expired via cache
  226. // tags locally. Because of this, page cache ignores max age.
  227. // - Get the tags from CacheableResponseInterface per the earlier comments.
  228. // - Get the time expiration from the Expires header, rather than the
  229. // interface, but see https://www.drupal.org/node/2352009 about possibly
  230. // changing that.
  231. $expire = 0;
  232. // 403 and 404 responses can fill non-LRU cache backends and generally are
  233. // likely to have a low cache hit rate. So do not cache them permanently.
  234. if ($response->isClientError()) {
  235. // Cache for an hour by default. If the 'cache_ttl_4xx' setting is
  236. // set to 0 then do not cache the response.
  237. $cache_ttl_4xx = Settings::get('cache_ttl_4xx', 3600);
  238. if ($cache_ttl_4xx > 0) {
  239. $expire = $request_time + $cache_ttl_4xx;
  240. }
  241. }
  242. else {
  243. $date = $response->getExpires()->getTimestamp();
  244. $expire = ($date > $request_time) ? $date : Cache::PERMANENT;
  245. }
  246. if ($expire === Cache::PERMANENT || $expire > $request_time) {
  247. $tags = $response->getCacheableMetadata()->getCacheTags();
  248. $this->set($request, $response, $expire, $tags);
  249. }
  250. // Mark response as a cache miss.
  251. $response->headers->set('X-Drupal-Cache', 'MISS');
  252. return $response;
  253. }
  254. /**
  255. * Returns a response object from the page cache.
  256. *
  257. * @param \Symfony\Component\HttpFoundation\Request $request
  258. * A request object.
  259. * @param bool $allow_invalid
  260. * (optional) If TRUE, a cache item may be returned even if it is expired or
  261. * has been invalidated. Such items may sometimes be preferred, if the
  262. * alternative is recalculating the value stored in the cache, especially
  263. * if another concurrent request is already recalculating the same value.
  264. * The "valid" property of the returned object indicates whether the item is
  265. * valid or not. Defaults to FALSE.
  266. *
  267. * @return \Symfony\Component\HttpFoundation\Response|false
  268. * The cached response or FALSE on failure.
  269. */
  270. protected function get(Request $request, $allow_invalid = FALSE) {
  271. $cid = $this->getCacheId($request);
  272. if ($cache = $this->cache->get($cid, $allow_invalid)) {
  273. return $cache->data;
  274. }
  275. return FALSE;
  276. }
  277. /**
  278. * Stores a response object in the page cache.
  279. *
  280. * @param \Symfony\Component\HttpFoundation\Request $request
  281. * A request object.
  282. * @param \Symfony\Component\HttpFoundation\Response $response
  283. * The response to store in the cache.
  284. * @param int $expire
  285. * One of the following values:
  286. * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should
  287. * not be removed unless it is deleted explicitly.
  288. * - A Unix timestamp: Indicates that the item will be considered invalid
  289. * after this time, i.e. it will not be returned by get() unless
  290. * $allow_invalid has been set to TRUE. When the item has expired, it may
  291. * be permanently deleted by the garbage collector at any time.
  292. * @param array $tags
  293. * An array of tags to be stored with the cache item. These should normally
  294. * identify objects used to build the cache item, which should trigger
  295. * cache invalidation when updated. For example if a cached item represents
  296. * a node, both the node ID and the author's user ID might be passed in as
  297. * tags. For example array('node' => array(123), 'user' => array(92)).
  298. */
  299. protected function set(Request $request, Response $response, $expire, array $tags) {
  300. $cid = $this->getCacheId($request);
  301. $this->cache->set($cid, $response, $expire, $tags);
  302. }
  303. /**
  304. * Gets the page cache ID for this request.
  305. *
  306. * @param \Symfony\Component\HttpFoundation\Request $request
  307. * A request object.
  308. *
  309. * @return string
  310. * The cache ID for this request.
  311. */
  312. protected function getCacheId(Request $request) {
  313. $cid_parts = array(
  314. $request->getUri(),
  315. $request->getRequestFormat(),
  316. );
  317. return implode(':', $cid_parts);
  318. }
  319. }

Classes

Namesort descending Description
PageCache Executes the page caching before the main kernel takes over the request.