diff --git a/core/core.services.yml b/core/core.services.yml
index 0f4d5863753f96f22878ae62300c850bdd228405..173f608c2be190b9a02e0980ba4ab3c2e6b66b1d 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -100,6 +100,18 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [discovery]
+ page_cache_request_policy:
+ class: Drupal\Core\PageCache\DefaultRequestPolicy
+ tags:
+ - { name: service_collector, tag: page_cache_request_policy, call: addPolicy}
+ page_cache_response_policy:
+ class: Drupal\Core\PageCache\ChainResponsePolicy
+ tags:
+ - { name: service_collector, tag: page_cache_response_policy, call: addPolicy}
+ page_cache_kill_switch:
+ class: Drupal\Core\PageCache\ResponsePolicy\KillSwitch
+ tags:
+ - { name: page_cache_response_policy }
config.manager:
class: Drupal\Core\Config\ConfigManager
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher']
@@ -729,7 +741,7 @@ services:
class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
tags:
- { name: event_subscriber }
- arguments: ['@language_manager', '@config.factory']
+ arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy']
redirect_response_subscriber:
class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber
arguments: ['@url_generator']
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 1e1cc30de218c23d5bacf1cc47647879cf6b652b..553811fc4377fa995abea5179f75f20e3919325d 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -389,30 +389,6 @@ function drupal_page_get_cache(Request $request) {
}
}
-/**
- * Determines the cacheability of the current page.
- *
- * Note: we do not serve cached pages to authenticated users, or to anonymous
- * users when $_SESSION is non-empty. $_SESSION may contain status messages
- * from a form submission, the contents of a shopping cart, or other user-
- * specific content that should not be cached and displayed to other users.
- *
- * @param $allow_caching
- * Set to FALSE if you want to prevent this page to get cached.
- *
- * @return
- * TRUE if the current page can be cached, FALSE otherwise.
- */
-function drupal_page_is_cacheable($allow_caching = NULL) {
- $allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
- if (isset($allow_caching)) {
- $allow_caching_static = $allow_caching;
- }
-
- return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
- && PHP_SAPI !== 'cli';
-}
-
/**
* Sets an HTTP response header for the current page.
*
@@ -931,7 +907,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
}
// Mark this page as being uncacheable.
- drupal_page_is_cacheable(FALSE);
+ \Drupal::service('page_cache_kill_switch')->trigger();
}
// Messages not set when DB connection fails.
diff --git a/core/includes/utility.inc b/core/includes/utility.inc
index 3192debd1c2464b53ad0c019c7a47afba4b40bb7..ec6a20cb2cc0a1c0d9f368e3bcce6326c5ebe0f5 100644
--- a/core/includes/utility.inc
+++ b/core/includes/utility.inc
@@ -62,8 +62,8 @@ function drupal_rebuild(ClassLoader $classloader, Request $request) {
$bin->deleteAll();
}
- // Disable the page cache.
- drupal_page_is_cacheable(FALSE);
+ // Disable recording of cached pages.
+ \Drupal::service('page_cache_kill_switch')->trigger();
drupal_flush_all_caches();
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 6f7b011a4d2f0238e8871a3abd385e69f080a612..065afae88890192c1d283dad586aa9a86dddf7ff 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -19,6 +19,7 @@
use Drupal\Core\DependencyInjection\YamlFileLoader;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Language\Language;
+use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Site\Settings;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -466,9 +467,8 @@ public function handlePageCache(Request $request) {
$cache_enabled = $config->get('cache.page.use_internal');
}
- // If there is no session cookie and cache is enabled (or forced), try to
- // serve a cached page.
- if (!$request->cookies->has(session_name()) && $cache_enabled && drupal_page_is_cacheable()) {
+ $request_policy = \Drupal::service('page_cache_request_policy');
+ if ($cache_enabled && $request_policy->check($request) === RequestPolicyInterface::ALLOW) {
// Get the page from the cache.
$response = drupal_page_get_cache($request);
// If there is a cached page, display it.
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index f882c2929ea069b450fb0d4eaa51252342ff0939..c5a7be1afe1ffe103054bf670c2d768134e07f8c 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -11,6 +11,8 @@
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicyInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -40,6 +42,20 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
*/
protected $config;
+ /**
+ * 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 FinishResponseSubscriber object.
*
@@ -47,10 +63,16 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
* The language manager object for retrieving the correct language code.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A config factory for retrieving required config objects.
+ * @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 a response.
*/
- public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory) {
+ public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) {
$this->languageManager = $language_manager;
$this->config = $config_factory->get('system.performance');
+ $this->requestPolicy = $request_policy;
+ $this->responsePolicy = $response_policy;
}
/**
@@ -83,16 +105,21 @@ public function onRespond(FilterResponseEvent $event) {
$response->headers->set($name, $value, FALSE);
}
- $is_cacheable = drupal_page_is_cacheable();
+ $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);
}
diff --git a/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php b/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php
new file mode 100644
index 0000000000000000000000000000000000000000..23dc1d6cca9d5731d6c564287b13e6710329ba2b
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php
@@ -0,0 +1,65 @@
+
+ *
Returns static::DENY if any of the rules evaluated to static::DENY
+ * Returns static::ALLOW if at least one of the rules evaluated to
+ * static::ALLOW and none to static::DENY
+ * Otherwise returns NULL
+ *
+ */
+class ChainRequestPolicy implements ChainRequestPolicyInterface {
+
+ /**
+ * A list of policy rules to apply when this policy is evaluated.
+ *
+ * @var \Drupal\Core\PageCache\RequestPolicyInterface[]
+ */
+ protected $rules = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Request $request) {
+ $final_result = NULL;
+
+ foreach ($this->rules as $rule) {
+ $result = $rule->check($request);
+ if ($result === static::DENY) {
+ return $result;
+ }
+ elseif ($result === static::ALLOW) {
+ $final_result = $result;
+ }
+ elseif (isset($result)) {
+ throw new \UnexpectedValueException('Return value of RequestPolicyInterface::check() must be one of RequestPolicyInterface::ALLOW, RequestPolicyInterface::DENY or NULL');
+ }
+ }
+
+ return $final_result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addPolicy(RequestPolicyInterface $policy) {
+ $this->rules[] = $policy;
+ return $this;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php b/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..e8548df7e59bdee6d323482ae79cd56fc530e4a9
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php
@@ -0,0 +1,25 @@
+
+ * Returns static::DENY if any of the rules evaluated to static::DENY
+ * Otherwise returns NULL
+ *
+ */
+class ChainResponsePolicy implements ChainResponsePolicyInterface {
+
+ /**
+ * A list of policy rules to apply when this policy is checked.
+ *
+ * @var \Drupal\Core\PageCache\ResponsePolicyInterface[]
+ */
+ protected $rules = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Response $response, Request $request) {
+ foreach ($this->rules as $rule) {
+ $result = $rule->check($response, $request);
+ if ($result === static::DENY) {
+ return $result;
+ }
+ elseif (isset($result)) {
+ throw new \UnexpectedValueException('Return value of ResponsePolicyInterface::check() must be one of ResponsePolicyInterface::DENY or NULL');
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addPolicy(ResponsePolicyInterface $policy) {
+ $this->rules[] = $policy;
+ return $this;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php b/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..5018f19abf41d8daa571189ba88f1d13f4a035ea
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php
@@ -0,0 +1,24 @@
+addPolicy(new RequestPolicy\CommandLineOrUnsafeMethod());
+ $this->addPolicy(new RequestPolicy\NoSessionOpen());
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php b/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php
new file mode 100644
index 0000000000000000000000000000000000000000..66a3bf692e3bb7cc7062346c8c806a2c6b70a336
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php
@@ -0,0 +1,38 @@
+isCli() || !$request->isMethodSafe()) {
+ return static::DENY;
+ }
+ }
+
+ /**
+ * Indicates whether this is a CLI request.
+ */
+ protected function isCli() {
+ return PHP_SAPI === 'cli';
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php b/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php
new file mode 100644
index 0000000000000000000000000000000000000000..c18cf5666033f95547d89b37e1621d7d34295e24
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php
@@ -0,0 +1,49 @@
+sessionCookieName = $session_cookie_name ?: session_name();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Request $request) {
+ if (!$request->cookies->has($this->sessionCookieName)) {
+ return static::ALLOW;
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php b/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..3a4ef212795af1b0a6c6068af087effe2659a1b4
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php
@@ -0,0 +1,54 @@
+kill) {
+ return static::DENY;
+ }
+ }
+
+ /**
+ * Deny any page caching on the current request.
+ */
+ public function trigger() {
+ $this->kill = TRUE;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..596f43fd9aaac7e83144a208b4f07ebe5b22a400
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php
@@ -0,0 +1,42 @@
+startNow();
- if ($user->isAuthenticated() || !$this->isSessionObsolete()) {
- drupal_page_is_cacheable(FALSE);
- }
}
if (empty($result)) {
diff --git a/core/modules/image/image.services.yml b/core/modules/image/image.services.yml
index 1447e036f61cb2dde760cdd471c793f2bca653a0..7f1f985f7367a0e232eec0a71cb85e55831e88ae 100644
--- a/core/modules/image/image.services.yml
+++ b/core/modules/image/image.services.yml
@@ -6,3 +6,8 @@ services:
plugin.manager.image.effect:
class: Drupal\image\ImageEffectManager
parent: default_plugin_manager
+ image.page_cache_request_policy.deny_private_image_style_download:
+ class: Drupal\image\PageCache\DenyPrivateImageStyleDownload
+ arguments: ['@current_route_match']
+ tags:
+ - { name: page_cache_response_policy }
diff --git a/core/modules/image/src/Controller/ImageStyleDownloadController.php b/core/modules/image/src/Controller/ImageStyleDownloadController.php
index 3e4bcf00d1486be4b46b583eab2dc42a29778ba3..a18c6774f478bf60197b4410d2fc2faea62e0e41 100644
--- a/core/modules/image/src/Controller/ImageStyleDownloadController.php
+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -155,7 +155,6 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
}
if ($success) {
- drupal_page_is_cacheable(FALSE);
$image = $this->imageFactory->get($derivative_uri);
$uri = $image->getSource();
$headers += array(
diff --git a/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php b/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php
new file mode 100644
index 0000000000000000000000000000000000000000..02cb27c88d541b469ab39b56a7c1e71570009be9
--- /dev/null
+++ b/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php
@@ -0,0 +1,49 @@
+routeMatch = $route_match;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Response $response, Request $request) {
+ if ($this->routeMatch->getRouteName() === 'image.style_private') {
+ return static::DENY;
+ }
+ }
+
+}
diff --git a/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php b/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fab54d36844cfb9be2e78a23cdb339c6296f2110
--- /dev/null
+++ b/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php
@@ -0,0 +1,90 @@
+routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+ $this->policy = new DenyPrivateImageStyleDownload($this->routeMatch);
+ $this->response = new Response();
+ $this->request = new Request();
+ }
+
+ /**
+ * Asserts that caching is denied on the private image style download route.
+ *
+ * @dataProvider providerPrivateImageStyleDownloadPolicy
+ * @covers ::check
+ */
+ public function testPrivateImageStyleDownloadPolicy($expected_result, $route_name) {
+ $this->routeMatch->expects($this->once())
+ ->method('getRouteName')
+ ->will($this->returnValue($route_name));
+
+ $actual_result = $this->policy->check($this->response, $this->request);
+ $this->assertSame($expected_result, $actual_result);
+ }
+
+ /**
+ * Provides data and expected results for the test method.
+ *
+ * @return array
+ * Data and expected results.
+ */
+ public function providerPrivateImageStyleDownloadPolicy() {
+ return [
+ [ResponsePolicyInterface::DENY, 'image.style_private'],
+ [NULL, 'some.other.route'],
+ [NULL, NULL],
+ [NULL, FALSE],
+ [NULL, TRUE],
+ [NULL, new \StdClass()],
+ [NULL, [1, 2, 3]],
+ ];
+ }
+
+}
diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml
index 546f47f4358c1c713af06d26ea54bf9309741b1e..68f0af1936b2ffbda07ebd6a54daba8ddf1ab416 100644
--- a/core/modules/node/node.services.yml
+++ b/core/modules/node/node.services.yml
@@ -34,3 +34,8 @@ services:
arguments: ['@user.tempstore']
tags:
- { name: paramconverter }
+ node.page_cache_request_policy.deny_node_preview:
+ class: Drupal\node\PageCache\DenyNodePreview
+ arguments: ['@current_route_match']
+ tags:
+ - { name: page_cache_response_policy }
diff --git a/core/modules/node/src/Controller/NodePreviewController.php b/core/modules/node/src/Controller/NodePreviewController.php
index 9fb8d13114069a05a9e89627ea7cff2a82a3f321..35c65d9654f09ccda0cb74b89a509a17f91b2b8b 100644
--- a/core/modules/node/src/Controller/NodePreviewController.php
+++ b/core/modules/node/src/Controller/NodePreviewController.php
@@ -20,9 +20,6 @@ class NodePreviewController extends EntityViewController {
* {@inheritdoc}
*/
public function view(EntityInterface $node_preview, $view_mode_id = 'full', $langcode = NULL) {
- // Do not cache this page.
- drupal_page_is_cacheable(FALSE);
-
$node_preview->preview_view_mode = $view_mode_id;
$build = array('nodes' => parent::view($node_preview, $view_mode_id));
diff --git a/core/modules/node/src/PageCache/DenyNodePreview.php b/core/modules/node/src/PageCache/DenyNodePreview.php
new file mode 100644
index 0000000000000000000000000000000000000000..5e749a049672f1f5cd299205d6645c2c860fee3b
--- /dev/null
+++ b/core/modules/node/src/PageCache/DenyNodePreview.php
@@ -0,0 +1,49 @@
+routeMatch = $route_match;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function check(Response $response, Request $request) {
+ if ($this->routeMatch->getRouteName() === 'entity.node.preview') {
+ return static::DENY;
+ }
+ }
+
+}
diff --git a/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea90c53952a0f08f782f0b54458b390cd689c1ff
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php
@@ -0,0 +1,90 @@
+routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+ $this->policy = new DenyNodePreview($this->routeMatch);
+ $this->response = new Response();
+ $this->request = new Request();
+ }
+
+ /**
+ * Asserts that caching is denied on the node preview route.
+ *
+ * @dataProvider providerPrivateImageStyleDownloadPolicy
+ * @covers ::check
+ */
+ public function testPrivateImageStyleDownloadPolicy($expected_result, $route_name) {
+ $this->routeMatch->expects($this->once())
+ ->method('getRouteName')
+ ->will($this->returnValue($route_name));
+
+ $actual_result = $this->policy->check($this->response, $this->request);
+ $this->assertSame($expected_result, $actual_result);
+ }
+
+ /**
+ * Provides data and expected results for the test method.
+ *
+ * @return array
+ * Data and expected results.
+ */
+ public function providerPrivateImageStyleDownloadPolicy() {
+ return [
+ [ResponsePolicyInterface::DENY, 'entity.node.preview'],
+ [NULL, 'some.other.route'],
+ [NULL, NULL],
+ [NULL, FALSE],
+ [NULL, TRUE],
+ [NULL, new \StdClass()],
+ [NULL, [1, 2, 3]],
+ ];
+ }
+
+}
diff --git a/core/modules/toolbar/src/Controller/ToolbarController.php b/core/modules/toolbar/src/Controller/ToolbarController.php
index 26639515a94d5a50c8b454177573cea562f957b6..aba53579a38d6fc011bc601e356b1b78eb9e281e 100644
--- a/core/modules/toolbar/src/Controller/ToolbarController.php
+++ b/core/modules/toolbar/src/Controller/ToolbarController.php
@@ -22,10 +22,22 @@ class ToolbarController extends ControllerBase {
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function subtreesJsonp() {
- _toolbar_initialize_page_cache();
$subtrees = toolbar_get_rendered_subtrees();
$response = new JsonResponse($subtrees);
$response->setCallback('Drupal.toolbar.setSubtrees.resolve');
+
+ // The Expires HTTP header is the heart of the client-side HTTP caching. The
+ // additional server-side page cache only takes effect when the client
+ // accesses the callback URL again (e.g., after clearing the browser cache
+ // or when force-reloading a Drupal page).
+ $max_age = 365 * 24 * 60 * 60;
+ $response->setPrivate();
+ $response->setMaxAge($max_age);
+
+ $expires = new \DateTime();
+ $expires->setTimestamp(REQUEST_TIME + $max_age);
+ $response->setExpires($expires);
+
return $response;
}
diff --git a/core/modules/toolbar/src/PageCache/AllowToolbarPath.php b/core/modules/toolbar/src/PageCache/AllowToolbarPath.php
new file mode 100644
index 0000000000000000000000000000000000000000..81d57348c9eecaca2d49e6fa19fc237e25085753
--- /dev/null
+++ b/core/modules/toolbar/src/PageCache/AllowToolbarPath.php
@@ -0,0 +1,32 @@
+getPathInfo())) {
+ return static::ALLOW;
+ }
+ }
+
+}
diff --git a/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php b/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1223be02e2a4555311b14e2c1b9513d9f8f9767
--- /dev/null
+++ b/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php
@@ -0,0 +1,65 @@
+policy = new AllowToolbarPath();
+ }
+
+ /**
+ * Asserts that caching is allowed if the request goes to toolbar subtree.
+ *
+ * @dataProvider providerTestAllowToolbarPath
+ * @covers ::check
+ */
+ public function testAllowToolbarPath($expected_result, $path) {
+ $request = Request::create($path);
+ $result = $this->policy->check($request);
+ $this->assertSame($expected_result, $result);
+ }
+
+ /**
+ * Provides data and expected results for the test method.
+ *
+ * @return array
+ * Data and expected results.
+ */
+ public function providerTestAllowToolbarPath() {
+ return [
+ [NULL, '/'],
+ [NULL, '/other-path?q=/toolbar/subtrees/'],
+ [NULL, '/toolbar/subtrees/'],
+ [NULL, '/toolbar/subtrees/some-hash/langcode/additional-stuff'],
+ [RequestPolicyInterface::ALLOW, '/de/toolbar/subtrees/abcd'],
+ [RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz'],
+ [RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz/de'],
+ [RequestPolicyInterface::ALLOW, '/a/b/c/toolbar/subtrees/xyz/de'],
+ [RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash'],
+ [RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash/en'],
+ ];
+ }
+
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 07deb3cb4c92ab4ecdfd68741e334a83988a84fa..964398105796a6bf47b9942ab3633d8d663d1ef8 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -95,43 +95,6 @@ function toolbar_element_info() {
return $elements;
}
-/**
- * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
- *
- * This gets invoked after full bootstrap, so must duplicate some of what's
- * done by \Drupal\Core\DrupalKernel::handlePageCache().
- *
- * @todo Replace this hack with something better integrated with DrupalKernel
- * once Drupal's page caching itself is properly integrated.
- */
-function _toolbar_initialize_page_cache() {
- $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
- drupal_page_is_cacheable(TRUE);
-
- // If we have a cache, serve it.
- // @see \Drupal\Core\DrupalKernel::handlePageCache()
- $request = \Drupal::request();
- $response = drupal_page_get_cache($request);
- if ($response) {
- $response->headers->set('X-Drupal-Cache', 'HIT');
-
- drupal_serve_page_from_cache($response, $request);
-
- $response->prepare($request);
- $response->send();
- // We are done.
- exit;
- }
-
- // The Expires HTTP header is the heart of the client-side HTTP caching. The
- // additional server-side page cache only takes effect when the client
- // accesses the callback URL again (e.g., after clearing the browser cache or
- // when force-reloading a Drupal page).
- $max_age = 3600 * 24 * 365;
- drupal_add_http_header('Expires', gmdate(DateTimePlus::RFC7231, REQUEST_TIME + $max_age));
- drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
-}
-
/**
* Implements hook_page_build().
*
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f269683c143d03c18f0440af7bced9a90be11a9..278d3c7313eb1e8f3ee2527cefc9d864337bc70b 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -6,3 +6,7 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [toolbar]
+ toolbar.page_cache_request_policy.allow_toolbar_path:
+ class: Drupal\toolbar\PageCache\AllowToolbarPath
+ tags:
+ - { name: page_cache_request_policy }
diff --git a/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php b/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..334373557dff10ef7c43821d647e788c6934b5dc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php
@@ -0,0 +1,165 @@
+policy = new ChainRequestPolicy();
+ $this->request = new Request();
+ }
+
+ /**
+ * Asserts that check() returns NULL if the chain is empty.
+ *
+ * @covers ::check
+ */
+ public function testEmptyChain() {
+ $result = $this->policy->check($this->request);
+ $this->assertSame(NULL, $result);
+ }
+
+ /**
+ * Asserts that check() returns NULL if a rule returns NULL.
+ *
+ * @covers ::check
+ */
+ public function testNullRuleChain() {
+ $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $rule->expects($this->once())
+ ->method('check')
+ ->with($this->request)
+ ->will($this->returnValue(NULL));
+
+ $this->policy->addPolicy($rule);
+
+ $result = $this->policy->check($this->request);
+ $this->assertSame(NULL, $result);
+ }
+
+ /**
+ * Asserts that check() throws an exception if a rule returns an invalid value.
+ *
+ * @expectedException \UnexpectedValueException
+ * @dataProvider providerChainExceptionOnInvalidReturnValue
+ * @covers ::check
+ */
+ public function testChainExceptionOnInvalidReturnValue($return_value) {
+ $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $rule->expects($this->once())
+ ->method('check')
+ ->with($this->request)
+ ->will($this->returnValue($return_value));
+
+ $this->policy->addPolicy($rule);
+
+ $this->policy->check($this->request);
+ }
+
+ /**
+ * Provides test data for testChainExceptionOnInvalidReturnValue.
+ *
+ * @return array
+ * Test input and expected result.
+ */
+ public function providerChainExceptionOnInvalidReturnValue() {
+ return [
+ [FALSE],
+ [0],
+ [1],
+ [TRUE],
+ [[1, 2, 3]],
+ [new \stdClass()],
+ ];
+ }
+
+ /**
+ * Asserts that check() returns ALLOW if any of the rules returns ALLOW.
+ *
+ * @dataProvider providerAllowIfAnyRuleReturnedAllow
+ * @covers ::check
+ */
+ public function testAllowIfAnyRuleReturnedAllow($return_values) {
+ foreach ($return_values as $return_value) {
+ $rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $rule->expects($this->once())
+ ->method('check')
+ ->with($this->request)
+ ->will($this->returnValue($return_value));
+
+ $this->policy->addPolicy($rule);
+ }
+
+ $actual_result = $this->policy->check($this->request);
+ $this->assertSame(RequestPolicyInterface::ALLOW, $actual_result);
+ }
+
+ /**
+ * Provides test data for testAllowIfAnyRuleReturnedAllow.
+ *
+ * @return array
+ * Test input and expected result.
+ */
+ public function providerAllowIfAnyRuleReturnedAllow() {
+ return [
+ [[RequestPolicyInterface::ALLOW]],
+ [[NULL, RequestPolicyInterface::ALLOW]],
+ ];
+ }
+
+ /**
+ * Asserts that check() returns immediately when a rule returned DENY.
+ */
+ public function testStopChainOnFirstDeny() {
+ $rule1 = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $rule1->expects($this->once())
+ ->method('check')
+ ->with($this->request)
+ ->will($this->returnValue(RequestPolicyInterface::ALLOW));
+ $this->policy->addPolicy($rule1);
+
+ $deny_rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $deny_rule->expects($this->once())
+ ->method('check')
+ ->with($this->request)
+ ->will($this->returnValue(RequestPolicyInterface::DENY));
+ $this->policy->addPolicy($deny_rule);
+
+ $ignored_rule = $this->getMock('Drupal\Core\PageCache\RequestPolicyInterface');
+ $ignored_rule->expects($this->never())
+ ->method('check');
+ $this->policy->addPolicy($ignored_rule);
+
+ $actual_result = $this->policy->check($this->request);
+ $this->assertsame(RequestPolicyInterface::DENY, $actual_result);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php b/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d9916782dba3b801056cd376e7ef2f08a4724a81
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php
@@ -0,0 +1,140 @@
+policy = new ChainResponsePolicy();
+ $this->response = new Response();
+ $this->request = new Request();
+ }
+
+ /**
+ * Asserts that check() returns NULL if the chain is empty.
+ *
+ * @covers ::check
+ */
+ public function testEmptyChain() {
+ $result = $this->policy->check($this->response, $this->request);
+ $this->assertSame(NULL, $result);
+ }
+
+ /**
+ * Asserts that check() returns NULL if a rule returns NULL.
+ *
+ * @covers ::check
+ */
+ public function testNullRuleChain() {
+ $rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface');
+ $rule->expects($this->once())
+ ->method('check')
+ ->with($this->response, $this->request)
+ ->will($this->returnValue(NULL));
+
+ $this->policy->addPolicy($rule);
+
+ $result = $this->policy->check($this->response, $this->request);
+ $this->assertSame(NULL, $result);
+ }
+
+ /**
+ * Asserts that check() throws an exception if a rule returns an invalid value.
+ *
+ * @expectedException \UnexpectedValueException
+ * @dataProvider providerChainExceptionOnInvalidReturnValue
+ * @covers ::check
+ */
+ public function testChainExceptionOnInvalidReturnValue($return_value) {
+ $rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface');
+ $rule->expects($this->once())
+ ->method('check')
+ ->with($this->response, $this->request)
+ ->will($this->returnValue($return_value));
+
+ $this->policy->addPolicy($rule);
+
+ $actual_result = $this->policy->check($this->response, $this->request);
+ $this->assertSame(NULL, $actual_result);
+ }
+
+ /**
+ * Provides test data for testChainExceptionOnInvalidReturnValue.
+ *
+ * @return array
+ * Test input and expected result.
+ */
+ public function providerChainExceptionOnInvalidReturnValue() {
+ return [
+ [FALSE],
+ [0],
+ [1],
+ [TRUE],
+ [[1, 2, 3]],
+ [new \stdClass()],
+ ];
+ }
+
+ /**
+ * Asserts that check() returns immediately when a rule returned DENY.
+ */
+ public function testStopChainOnFirstDeny() {
+ $rule1 = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface');
+ $rule1->expects($this->once())
+ ->method('check')
+ ->with($this->response, $this->request);
+ $this->policy->addPolicy($rule1);
+
+ $deny_rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface');
+ $deny_rule->expects($this->once())
+ ->method('check')
+ ->with($this->response, $this->request)
+ ->will($this->returnValue(ResponsePolicyInterface::DENY));
+ $this->policy->addPolicy($deny_rule);
+
+ $ignored_rule = $this->getMock('Drupal\Core\PageCache\ResponsePolicyInterface');
+ $ignored_rule->expects($this->never())
+ ->method('check');
+ $this->policy->addPolicy($ignored_rule);
+
+ $actual_result = $this->policy->check($this->response, $this->request);
+ $this->assertsame(ResponsePolicyInterface::DENY, $actual_result);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php b/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb54fbca050bb7de9845821354ed858b02b32d4d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php
@@ -0,0 +1,83 @@
+policy = $this->getMock('Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod', array('isCli'));
+ }
+
+ /**
+ * Asserts that check() returns DENY for unsafe HTTP methods.
+ *
+ * @dataProvider providerTestHttpMethod
+ * @covers ::check
+ */
+ public function testHttpMethod($expected_result, $method) {
+ $this->policy->expects($this->once())
+ ->method('isCli')
+ ->will($this->returnValue(FALSE));
+
+ $request = Request::create('/', $method);
+ $actual_result = $this->policy->check($request);
+ $this->assertSame($expected_result, $actual_result);
+ }
+
+ /**
+ * Provides test data and expected results for the HTTP method test.
+ *
+ * @return array
+ * Test data and expected results.
+ */
+ public function providerTestHttpMethod() {
+ return [
+ [NULL, 'GET'],
+ [NULL, 'HEAD'],
+ [RequestPolicyInterface::DENY, 'POST'],
+ [RequestPolicyInterface::DENY, 'PUT'],
+ [RequestPolicyInterface::DENY, 'DELETE'],
+ [RequestPolicyInterface::DENY, 'OPTIONS'],
+ [RequestPolicyInterface::DENY, 'TRACE'],
+ [RequestPolicyInterface::DENY, 'CONNECT'],
+ ];
+ }
+
+ /**
+ * Asserts that check() returns DENY if running from the command line.
+ *
+ * @covers ::check
+ */
+ public function testIsCli() {
+ $this->policy->expects($this->once())
+ ->method('isCli')
+ ->will($this->returnValue(TRUE));
+
+ $request = Request::create('/', 'GET');
+ $actual_result = $this->policy->check($request);
+ $this->assertSame(RequestPolicyInterface::DENY, $actual_result);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php b/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f0fe4cfca20c66a7075f1850fc67bf0c8dd8478c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php
@@ -0,0 +1,54 @@
+sessionCookieName = 'B1ESkdf3V4F8u27myaSAShuuHc';
+ $this->policy = new RequestPolicy\NoSessionOpen($this->sessionCookieName);
+ }
+
+ /**
+ * Asserts that caching is allowed unless there is a session cookie present.
+ *
+ * @covers ::check
+ */
+ public function testNoAllowUnlessSessionCookiePresent() {
+ $request = new Request();
+ $result = $this->policy->check($request);
+ $this->assertSame(RequestPolicyInterface::ALLOW, $result);
+
+ $request = Request::create('/', 'GET', [], [$this->sessionCookieName => 'some-session-id']);
+ $result = $this->policy->check($request);
+ $this->assertSame(NULL, $result);
+ }
+}