diff --git a/core/core.services.yml b/core/core.services.yml
index 0f4d586..173f608 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 1e1cc30..553811f 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -390,30 +390,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.
  *
  * Note: When sending a Content-Type header, always include a 'charset' type,
@@ -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 3192deb..ec6a20c 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 6f7b011..065afae 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 f882c29..c5a7be1 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;
@@ -41,16 +43,36 @@ 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.
    *
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   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 0000000..23dc1d6
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicy.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ChainRequestPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Implements a compound request policy.
+ *
+ * When evaluating the compound policy, all of the contained rules are applied
+ * to the request. The overall result is computed according to the following
+ * rules:
+ *
+ * <ol>
+ *   <li>Returns static::DENY if any of the rules evaluated to static::DENY</li>
+ *   <li>Returns static::ALLOW if at least one of the rules evaluated to
+ *       static::ALLOW and none to static::DENY</li>
+ *   <li>Otherwise returns NULL</li>
+ * </ol>
+ */
+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 0000000..e8548df
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainRequestPolicyInterface.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ChainRequestPolicyInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * Defines the interface for compound request policies.
+ */
+interface ChainRequestPolicyInterface extends RequestPolicyInterface {
+
+  /**
+   * Add a policy to the list of policy rules.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyInterface $policy
+   *   The request policy rule to add.
+   *
+   * @return $this
+   */
+  public function addPolicy(RequestPolicyInterface $policy);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php b/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php
new file mode 100644
index 0000000..20d5e39
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainResponsePolicy.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ChainResponsePolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Implements a compound response policy.
+ *
+ * When evaluating the compound policy, all of the contained rules are applied
+ * to the response. The overall result is computed according to the following
+ * rules:
+ *
+ * <ol>
+ *   <li>Returns static::DENY if any of the rules evaluated to static::DENY</li>
+ *   <li>Otherwise returns NULL</li>
+ * </ol>
+ */
+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 0000000..5018f19
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ChainResponsePolicyInterface.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ChainResponsePolicyInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * Defines the interface for compound request policies.
+ */
+interface ChainResponsePolicyInterface extends ResponsePolicyInterface {
+
+  /**
+   * Add a policy to the list of policy rules.
+   *
+   * @param \Drupal\Core\PageCache\ResponsePolicyInterface $policy
+   *   The request policy rule to add.
+   *
+   * @return $this
+   */
+  public function addPolicy(ResponsePolicyInterface $policy);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php
new file mode 100644
index 0000000..b327753
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DefaultRequestPolicy.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DefaultRequestPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * The default page cache request policy.
+ *
+ * Delivery of cached pages is denied if either the application is running from
+ * the command line or the request was not initiated with a safe method (GET or
+ * HEAD). Also caching is only allowed for requests without a session cookie.
+ */
+class DefaultRequestPolicy extends ChainRequestPolicy {
+
+  /**
+   * Constructs the default page cache request policy.
+   */
+  public function __construct() {
+    $this->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 0000000..66a3bf6
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/CommandLineOrUnsafeMethod.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicy;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Reject when running from the command line or when HTTP method is not safe.
+ *
+ * The policy denies caching if the request was initiated from the command line
+ * interface (drush) or the request method is neither GET nor HEAD (see RFC
+ * 2616, section 9.1.1 - Safe Methods).
+ */
+class CommandLineOrUnsafeMethod implements RequestPolicyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Request $request) {
+    if ($this->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 0000000..c18cf56
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy/NoSessionOpen.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicy;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy allowing delivery of cached pages when there is no session open.
+ *
+ * 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 userspecific content
+ * that should not be cached and displayed to other users.
+ */
+class NoSessionOpen implements RequestPolicyInterface {
+
+  /**
+   * The name of the session cookie.
+   *
+   * @var string
+   */
+  protected $sessionCookieName;
+
+  /**
+   * Constructs a new page cache session policy.
+   *
+   * @param string $session_cookie_name
+   *   (optional) The name of the session cookie. Defaults to session_name().
+   */
+  public function __construct($session_cookie_name = NULL) {
+    $this->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 0000000..3a4ef21
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for request policy implementations.
+ *
+ * The request policy is evaluated in order to determine whether delivery of a
+ * cached page should be attempted. The caller should do so if static::ALLOW is
+ * returned from the check() method.
+ */
+interface RequestPolicyInterface {
+
+  /**
+   * Allow delivery of cached pages.
+   */
+  const ALLOW = 'allow';
+
+  /**
+   * Deny delivery of cached pages.
+   */
+  const DENY = 'deny';
+
+  /**
+   * Determines whether delivery of a cached page should be attempted.
+   *
+   * Note that the request-policy check runs very early. In particular it is
+   * not possible to determine the logged in user. Also the current route match
+   * is not yet present when the check runs. Therefore, request-policy checks
+   * need to be designed in a way such that they do not depend on any other
+   * service and only take in account the information present on the incoming
+   * request.
+   *
+   * When matching against the request path, special attention is needed to
+   * support path prefixes which are often used on multilingual sites.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The incoming request object.
+   *
+   * @return string|NULL
+   *   One of static::ALLOW, static::DENY or NULL. Calling code may attempt to
+   *   deliver a cached page if static::ALLOW is returned. Returns NULL if the
+   *   policy is not specified for the given request.
+   */
+  public function check(Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php
new file mode 100644
index 0000000..62ecc45
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy/KillSwitch.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicy\KillSwitch.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicy;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A policy evaluating to static::DENY when the kill switch was triggered.
+ */
+class KillSwitch implements ResponsePolicyInterface {
+
+  /**
+   * A flag indicating whether the kill switch was triggered.
+   *
+   * @var bool
+   */
+  protected $kill = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Response $response, Request $request) {
+    if ($this->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 0000000..596f43f
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines the interface for response policy implementations.
+ *
+ * The response policy is evaluated in order to determine whether a page should
+ * be stored a in the cache. Calling code should do so unless static::DENY is
+ * returned from the check() method.
+ */
+interface ResponsePolicyInterface {
+
+  /**
+   * Deny storage of a page in the cache.
+   */
+  const DENY = 'deny';
+
+  /**
+   * Determines whether it is save to store a page in the cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response which is about to be sent to the client.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @return string|NULL
+   *   Either static::DENY or NULL. Calling code may attempt to store a page in
+   *   the cache unless static::DENY is returned. Returns NULL if the policy
+   *   policy is not specified for the given response.
+   */
+  public function check(Response $response, Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index 64b8fdd..efb3b7a 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -130,9 +130,6 @@ public function start() {
       // anonymous users not use a session cookie unless something is stored in
       // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
       $result = $this->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 1447e03..7f1f985 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 3e4bcf0..a18c677 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 0000000..02cb27c
--- /dev/null
+++ b/core/modules/image/src/PageCache/DenyPrivateImageStyleDownload.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image\PageCache\DenyPrivateImageStyleDownload.
+ */
+
+namespace Drupal\image\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache policy for image preview page.
+ *
+ * This policy rule denies caching of responses generated by the
+ * entity.image.preview route.
+ */
+class DenyPrivateImageStyleDownload implements ResponsePolicyInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a deny image preview page cache policy.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(RouteMatchInterface $route_match) {
+    $this->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 0000000..fab54d3
--- /dev/null
+++ b/core/modules/image/tests/src/Unit/PageCache/DenyPrivateImageStyleDownloadTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\image\Unit\PageCache\DenyPrivateImageStyleDownloadTest.
+ */
+
+namespace Drupal\Tests\image\Unit\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\image\PageCache\DenyPrivateImageStyleDownload;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * @coversDefaultClass \Drupal\image\PageCache\DenyPrivateImageStyleDownload
+ * @group image
+ */
+class DenyPrivateImageStyleDownloadTest extends UnitTestCase {
+
+  /**
+   * The response policy under test.
+   *
+   * @var \Drupal\image\PageCache\DenyPrivateImageStyleDownload
+   */
+  protected $policy;
+
+  /**
+   * A request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * A response object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Response
+   */
+  protected $response;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeMatch;
+
+  public function setUp() {
+    $this->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 546f47f..68f0af1 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 9fb8d13..35c65d9 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 0000000..5e749a0
--- /dev/null
+++ b/core/modules/node/src/PageCache/DenyNodePreview.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\PageCache\DenyNodePreview.
+ */
+
+namespace Drupal\node\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache policy for node preview page.
+ *
+ * This policy rule denies caching of responses generated by the
+ * entity.node.preview route.
+ */
+class DenyNodePreview implements ResponsePolicyInterface {
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a deny node preview page cache policy.
+   *
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(RouteMatchInterface $route_match) {
+    $this->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 0000000..ea90c53
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\node\Unit\PageCache\DenyNodePreviewTest.
+ */
+
+namespace Drupal\Tests\node\Unit\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\node\PageCache\DenyNodePreview;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * @coversDefaultClass \Drupal\node\PageCache\DenyNodePreview
+ * @group node
+ */
+class DenyNodePreviewTest extends UnitTestCase {
+
+  /**
+   * The response policy under test.
+   *
+   * @var \Drupal\node\PageCache\DenyNodePreview
+   */
+  protected $policy;
+
+  /**
+   * A request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * A response object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Response
+   */
+  protected $response;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeMatch;
+
+  public function setUp() {
+    $this->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 2663951..aba5357 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 0000000..81d5734
--- /dev/null
+++ b/core/modules/toolbar/src/PageCache/AllowToolbarPath.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\toolbar\PageCache\AllowToolbarPath.
+ */
+
+namespace Drupal\toolbar\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for the toolbar page cache service.
+ *
+ * This policy allows caching of requests directed to /toolbar/subtrees/{hash}
+ * even for authenticated users.
+ */
+class AllowToolbarPath implements RequestPolicyInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function check(Request $request) {
+    // Note that this regular expression matches the end of pathinfo in order to
+    // support multilingual sites using path prefixes.
+    if (preg_match('#/toolbar/subtrees/[^/]+(/[^/]+)?$#', $request->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 0000000..f1223be
--- /dev/null
+++ b/core/modules/toolbar/tests/src/Unit/PageCache/AllowToolbarPathTest.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\toolbar\Unit\PageCache\AllowToolbarPathTest.
+ */
+
+namespace Drupal\Tests\toolbar\Unit\PageCache;
+
+use Drupal\toolbar\PageCache\AllowToolbarPath;
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\toolbar\PageCache\AllowToolbarPath
+ * @group toolbar
+ */
+class AllowToolbarPathTest extends UnitTestCase {
+
+  /**
+   * The toolbar path policy under test.
+   *
+   * @var \Drupal\toolbar\PageCache\AllowToolbarPath
+   */
+  protected $policy;
+
+  public function setUp() {
+    $this->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 07deb3c..9643981 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -96,43 +96,6 @@ function toolbar_element_info() {
 }
 
 /**
- * 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().
  *
  * Add admin toolbar to the page_top region automatically.
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f26968..278d3c7 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 0000000..3343735
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/ChainRequestPolicyTest.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\PageCache\ChainRequestPolicyTest.
+ */
+
+namespace Drupal\Tests\Core\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\ChainRequestPolicy;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\PageCache\ChainRequestPolicy
+ * @group PageCache
+ */
+class ChainRequestPolicyTest extends UnitTestCase {
+
+  /**
+   * The chain request policy under test.
+   *
+   * @var \Drupal\Core\PageCache\ChainRequestPolicy
+   */
+  protected $policy;
+
+  /**
+   * A request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  public function setUp() {
+    $this->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 0000000..d991678
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/ChainResponsePolicyTest.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\PageCache\ChainResponsePolicyTest.
+ */
+
+namespace Drupal\Tests\Core\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\PageCache\ChainResponsePolicy;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * @coversDefaultClass \Drupal\Core\PageCache\ChainResponsePolicy
+ * @group PageCache
+ */
+class ChainResponsePolicyTest extends UnitTestCase {
+
+  /**
+   * The chain response policy under test.
+   *
+   * @var \Drupal\Core\PageCache\ChainResponsePolicy
+   */
+  protected $policy;
+
+  /**
+   * A request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * A response object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Response
+   */
+  protected $response;
+
+  public function setUp() {
+    $this->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 0000000..bb54fbc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/CommandLineOrUnsafeMethodTest.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\PageCache\CommandLineOrUnsafeMethodTest.
+ */
+
+namespace Drupal\Tests\Core\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod
+ * @group PageCache
+ */
+class CommandLineOrUnsafeMethodTest extends UnitTestCase {
+
+  /**
+   * The request policy under test.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $policy;
+
+  public function setUp() {
+    // Note that it is necessary to partially mock the class under test in
+    // order to disable the isCli-check.
+    $this->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 0000000..f0fe4cf
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/NoSessionOpenTest.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\PageCache\NoSessionOpenTest.
+ */
+
+namespace Drupal\Tests\Core\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyInterface;
+use Drupal\Core\PageCache\RequestPolicy;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen
+ * @group PageCache
+ */
+class NoSessionOpenTest extends UnitTestCase {
+
+  /**
+   * The session cookie name.
+   *
+   * @var string
+   */
+  protected $sessionCookieName;
+
+  /**
+   * The request policy under test.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicy\NoSessionOpen
+   */
+  protected $policy;
+
+  public function setUp() {
+    $this->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);
+  }
+}
