diff --git a/core/core.services.yml b/core/core.services.yml
index f160803..2b1a3da 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -66,6 +66,11 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [path]
+  page_cache.internal:
+    class: Drupal\Core\PageCache\InternalPageCache
+    arguments: ['@cache.page', '@content_negotiation', '@config.factory', '@settings']
+  page_cache.policy:
+    class: Drupal\Core\PageCache\DefaultPageCachePolicy
   config.cachedstorage.storage:
     class: Drupal\Core\Config\FileStorage
     factory_class: Drupal\Core\Config\FileStorageFactory
@@ -499,7 +504,7 @@ services:
     class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@language_manager']
+    arguments: ['@language_manager', '@page_cache.policy', '@page_cache.internal', '@config.factory']
     scope: request
   redirect_response_subscriber:
     class: Drupal\Core\EventSubscriber\RedirectResponseSubscriber
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 8bbdd91..19b6ad0 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -7,6 +7,7 @@
 use Drupal\Component\Utility\Timer;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\Url;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -910,62 +911,6 @@ function variable_del($name) {
 }
 
 /**
- * Gets the page cache cid for this request.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return string
- *   The cid for this request.
- */
-function drupal_page_cache_get_cid(Request $request) {
-  $cid_parts = array(
-    $request->getUri(),
-    \Drupal::service('content_negotiation')->getContentType($request),
-  );
-  return sha1(implode(':', $cid_parts));
-}
-
-/**
- * Retrieves the current page from the cache.
- *
- * 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 \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return
- *   The cache object, if the page was found in the cache, NULL otherwise.
- */
-function drupal_page_get_cache(Request $request) {
-  if (drupal_page_is_cacheable()) {
-    return \Drupal::cache('page')->get(drupal_page_cache_get_cid($request));
-  }
-}
-
-/**
- * Determines the cacheability of the current page.
- *
- * @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')
-    && !drupal_is_cli();
-}
-
-/**
  * Includes a file with the provided type and name.
  *
  * This prevents including a theme, engine, module, etc., more than once.
@@ -1168,106 +1113,6 @@ function drupal_page_header() {
 }
 
 /**
- * Sets HTTP headers in preparation for a cached page response.
- *
- * The headers allow as much as possible in proxies and browsers without any
- * particular knowledge about the pages. Modules can override these headers
- * using drupal_add_http_header().
- *
- * If the request is conditional (using If-Modified-Since and If-None-Match),
- * and the conditions match those currently in the cache, a 304 Not Modified
- * response is sent.
- */
-function drupal_serve_page_from_cache(stdClass $cache, Response $response, Request $request) {
-  $config = \Drupal::config('system.performance');
-
-  // First half: we must determine if we should be returning a 304.
-
-  // Negotiate whether to use compression.
-  $page_compression = !empty($cache->data['page_compressed']) && extension_loaded('zlib');
-  $return_compressed = $page_compression && $request->server->has('HTTP_ACCEPT_ENCODING') && strpos($request->server->get('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE;
-
-  // Get headers. Keys are lower-case.
-  $boot_headers = drupal_get_http_header();
-
-  foreach ($cache->data['headers'] as $name => $value) {
-    // In the case of a 304 response, certain headers must be sent, and the
-    // remaining may not (see RFC 2616, section 10.3.5).
-    $name_lower = strtolower($name);
-    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
-      $response->headers->set($name, $value);
-      unset($cache->data['headers'][$name]);
-    }
-  }
-
-  // If the client sent a session cookie, a cached copy will only be served
-  // to that one particular client due to Vary: Cookie. Thus, do not set
-  // max-age > 0, allowing the page to be cached by external proxies, when a
-  // session cookie is present unless the Vary header has been replaced.
-  $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
-  $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
-
-  // Entity tag should change if the output changes.
-  $response->setEtag($cache->created);
-
-  // See if the client has provided the required HTTP headers.
-  $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
-  $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
-
-  if ($if_modified_since && $if_none_match
-      && $if_none_match == $response->headers->get('etag') // etag must match
-      && $if_modified_since == $cache->created) {  // if-modified-since must match
-    $response->setStatusCode(304);
-    return;
-  }
-
-  // Second half: we're not returning a 304, so put in other headers.
-
-  // Send the remaining headers.
-  foreach ($cache->data['headers'] as $name => $value) {
-    $response->headers->set($name, $value);
-    drupal_add_http_header($name, $value);
-  }
-
-  $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
-
-  // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
-  // by sending an Expires date in the past. HTTP/1.1 clients ignores the
-  // Expires header if a Cache-Control: max-age= directive is specified (see RFC
-  // 2616, section 14.9.3).
-  if (!$response->getExpires()) {
-    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-  }
-
-  // Allow HTTP proxies to cache pages for anonymous users without a session
-  // cookie. The Vary header is used to indicates the set of request-header
-  // fields that fully determines whether a cache is permitted to use the
-  // response to reply to a subsequent request for a given URL without
-  // revalidation.
-  if (!isset($boot_headers['vary']) && !settings()->get('omit_vary_cookie')) {
-    $response->setVary('Cookie', FALSE);
-  }
-
-  if ($page_compression) {
-    $response->setVary('accept-encoding', FALSE);
-    // If page_compression is enabled, the cache contains gzipped data.
-    if ($return_compressed) {
-      // $cache->data['body'] is already gzip'ed, so make sure
-      // zlib.output_compression does not compress it once more.
-      ini_set('zlib.output_compression', '0');
-      $response->headers->set('content-encoding', 'gzip');
-    }
-    else {
-      // The client does not support compression, so unzip the data in the
-      // cache. Strip the gzip header and run uncompress.
-      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
-    }
-  }
-
-  $response->setContent($cache->data['body']);
-}
-
-/**
  * Translates a string to the current language or to a given language.
  *
  * The t() function serves two purposes. First, at run-time it translates
@@ -1579,7 +1424,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.policy')->cancel();
   }
 
   // Messages not set when DB connection fails.
@@ -1775,10 +1620,10 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
           break;
 
         case DRUPAL_BOOTSTRAP_PAGE_CACHE:
-          _drupal_bootstrap_page_cache();
           break;
 
         case DRUPAL_BOOTSTRAP_VARIABLES:
+          require_once __DIR__ . '/database.inc';
           _drupal_bootstrap_variables();
           break;
 
@@ -1837,6 +1682,19 @@ function drupal_handle_request($test_only = FALSE) {
   $request = Request::createFromGlobals();
   \Drupal::getContainer()->set('request', $request);
 
+  // Apply the current cache policy to the request.
+  $cache_policy = \Drupal::service('page_cache.policy');
+  $cache_policy->applyToRequest($request);
+
+  // Attempt to serve a cached response from the internal page cache.
+  $use_internal_cache = \Drupal::config('system.performance')->get('cache.page.use_internal');
+  if ($use_internal_cache && $cache_policy->isCacheable()) {
+    $page_cache = \Drupal::service('page_cache.internal');
+    if ($page_cache->deliver($request)) {
+      exit();
+    }
+  }
+
   drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
 
   $response = $kernel->handle($request)->prepare($request)->send();
@@ -1974,54 +1832,6 @@ function _drupal_bootstrap_kernel() {
 }
 
 /**
- * Attempts to serve a page from the cache.
- */
-function _drupal_bootstrap_page_cache() {
-  global $user;
-
-  require_once __DIR__ . '/cache.inc';
-  require_once __DIR__ . '/database.inc';
-  // Check for a cache mode force from settings.php.
-  if (settings()->get('page_cache_without_database')) {
-    $cache_enabled = TRUE;
-  }
-  else {
-    drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
-    $config = \Drupal::config('system.performance');
-    $cache_enabled = $config->get('cache.page.use_internal');
-  }
-
-  $request = Request::createFromGlobals();
-  // 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) {
-    // Make sure there is a user object because its timestamp will be checked.
-    $user = drupal_anonymous_user();
-    // Get the page from the cache.
-    $cache = drupal_page_get_cache($request);
-    // If there is a cached page, display it.
-    if (is_object($cache)) {
-      $response = new Response();
-      $response->headers->set('X-Drupal-Cache', 'HIT');
-      // Restore the metadata cached with the page.
-      _current_path($cache->data['path']);
-      drupal_set_title($cache->data['title'], PASS_THROUGH);
-      date_default_timezone_set(drupal_get_user_timezone());
-
-      drupal_serve_page_from_cache($cache, $response, $request);
-
-      // We are done.
-      $response->prepare($request);
-      $response->send();
-      exit;
-    }
-    else {
-      drupal_add_http_header('X-Drupal-Cache', 'MISS');
-    }
-  }
-}
-
-/**
  * In a test environment, get the test db prefix and set it in $databases.
  */
 function _drupal_initialize_db_test_prefix() {
@@ -2811,6 +2621,57 @@ function drupal_check_memory_limit($required, $memory_limit = NULL) {
 }
 
 /**
+ * @defgroup cache Functions for cache handling
+ * @{
+ */
+
+/**
+ * Instantiates and statically caches the correct class for a cache bin.
+ *
+ * By default, this returns an instance of the Drupal\Core\Cache\DatabaseBackend
+ * class.
+ *
+ * Classes implementing Drupal\Core\Cache\CacheBackendInterface can register
+ * themselves both as a default implementation and for specific bins.
+ *
+ * @param string $bin
+ *   The cache bin for which the cache object should be returned, defaults to
+ *   'cache'.
+ *
+ * @return \Drupal\Core\Cache\CacheBackendInterface
+ *   The cache object associated with the specified bin.
+ *
+ * @see \Drupal\Core\Cache\CacheBackendInterface
+ */
+function cache($bin = 'cache') {
+  return \Drupal::cache($bin);
+}
+
+/**
+ * Marks cache items from all bins with any of the specified tags as invalid.
+ *
+ * Many sites have more than one active cache backend, and each backend my use
+ * a different strategy for storing tags against cache items, and invalidating
+ * cache items associated with a given tag.
+ *
+ * When invalidating a given list of tags, we iterate over each cache backend,
+ * and call invalidateTags() on each.
+ *
+ * @param array $tags
+ *   The list of tags to invalidate cache items for.
+ *
+ * @deprecated 8.x
+ *   Use \Drupal\Core\Cache\Cache::invalidateTags().
+ */
+function cache_invalidate_tags(array $tags) {
+  Cache::invalidateTags($tags);
+}
+
+/**
+ * @} End of "defgroup lock".
+ */
+
+/**
  * @defgroup lock Locking mechanisms
  * @{
  * Functions to coordinate long-running operations across requests.
diff --git a/core/includes/cache.inc b/core/includes/cache.inc
deleted file mode 100644
index b39b7ff..0000000
--- a/core/includes/cache.inc
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions and interfaces for cache handling.
- */
-
-use Drupal\Core\Cache\Cache;
-
-/**
- * Instantiates and statically caches the correct class for a cache bin.
- *
- * By default, this returns an instance of the Drupal\Core\Cache\DatabaseBackend
- * class.
- *
- * Classes implementing Drupal\Core\Cache\CacheBackendInterface can register
- * themselves both as a default implementation and for specific bins.
- *
- * @param $bin
- *   The cache bin for which the cache object should be returned, defaults to
- *   'cache'.
- *
- * @return \Drupal\Core\Cache\CacheBackendInterface
- *   The cache object associated with the specified bin.
- *
- * @see \Drupal\Core\Cache\CacheBackendInterface
- */
-function cache($bin = 'cache') {
-  return \Drupal::cache($bin);
-}
-
-/**
- * Marks cache items from all bins with any of the specified tags as invalid.
- *
- * Many sites have more than one active cache backend, and each backend my use
- * a different strategy for storing tags against cache items, and invalidating
- * cache items associated with a given tag.
- *
- * When invalidating a given list of tags, we iterate over each cache backend,
- * and call invalidateTags() on each.
- *
- * @param array $tags
- *   The list of tags to invalidate cache items for.
- *
- * @deprecated 8.x
- *   Use \Drupal\Core\Cache\Cache::invalidateTags().
- */
-function cache_invalidate_tags(array $tags) {
-  Cache::invalidateTags($tags);
-}
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 8eec0ac..9e72922 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3113,70 +3113,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
 }
 
 /**
- * Stores the current page in the cache.
- *
- * If page_compression is enabled, a gzipped version of the page is stored in
- * the cache to avoid compressing the output on each request. The cache entry
- * is unzipped in the relatively rare event that the page is requested by a
- * client without gzip support.
- *
- * Page compression requires the PHP zlib extension
- * (http://php.net/manual/ref.zlib.php).
- *
- * @param $body
- *   The response body.
- * @return
- *   The cached object or NULL if the page cache was not set.
- *
- * @see drupal_page_header()
- */
-function drupal_page_set_cache(Response $response, Request $request) {
-  if (drupal_page_is_cacheable()) {
-
-    // Check if the current page may be compressed.
-    $page_compressed = \Drupal::config('system.performance')->get('response.gzip') && extension_loaded('zlib');
-
-    $cache = (object) array(
-      'cid' => drupal_page_cache_get_cid($request),
-      'data' => array(
-        'path' => $request->attributes->get('_system_path'),
-        'body' => $response->getContent(),
-        'title' => drupal_get_title(),
-        'headers' => array(),
-        // We need to store whether page was compressed or not,
-        // because by the time it is read, the configuration might change.
-        'page_compressed' => $page_compressed,
-      ),
-      'tags' => array('content' => TRUE) + drupal_cache_tags_page_get(),
-      'expire' => Cache::PERMANENT,
-      'created' => REQUEST_TIME,
-    );
-
-    $cache->data['headers'] = $response->headers->all();
-
-    // Hack: exclude the x-drupal-cache header; it may make it in here because
-    // of awkwardness in how we defer sending it over in _drupal_page_get_cache.
-    if (isset($cache->data['headers']['x-drupal-cache'])) {
-      unset($cache->data['headers']['x-drupal-cache']);
-    }
-
-     // Use the actual timestamp from an Expires header, if available.
-    if ($date = $response->getExpires()) {
-      $date = DrupalDateTime::createFromDateTime($date);
-      $cache->expire = $date->getTimestamp();
-    }
-
-    if ($cache->data['body']) {
-      if ($page_compressed) {
-        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
-      }
-      cache('page')->set($cache->cid, $cache->data, $cache->expire, $cache->tags);
-    }
-    return $cache;
-  }
-}
-
-/**
  * This function is kept only for backward compatibility.
  *
  * @see \Drupal\Core\SystemListing::scan().
@@ -4371,7 +4307,7 @@ function drupal_render_collect_cache_tags($element, $tags = array()) {
  *   The same $children that was passed in - no modifications.
  */
 function drupal_post_render_cache_tags_page_set($children, array $elements) {
-  if (drupal_page_is_cacheable()) {
+  if (\Drupal::service('page_cache.policy')->isCacheable()) {
     $tags = &drupal_static('system_cache_tags_page', array());
     $tags = drupal_render_collect_cache_tags($elements);
   }
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 0259cec..e4dc93f 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -320,7 +320,6 @@ function install_begin_request(&$install_state) {
   require_once __DIR__ . '/install.inc';
   require_once __DIR__ . '/schema.inc';
   require_once __DIR__ . '/../../' . settings()->get('path_inc', 'core/includes/path.inc');
-  require_once __DIR__ . '/cache.inc';
   require_once __DIR__ . '/database.inc';
   require_once __DIR__ . '/form.inc';
   require_once __DIR__ . '/batch.inc';
diff --git a/core/includes/session.inc b/core/includes/session.inc
index fa46972..378dfbb 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -251,7 +251,7 @@ function drupal_session_initialize() {
     // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
     drupal_session_start();
     if ($user->isAuthenticated() || !empty($_SESSION)) {
-      drupal_page_is_cacheable(FALSE);
+      \Drupal::service('page_cache.policy')->cancel();
     }
   }
   else {
diff --git a/core/includes/utility.inc b/core/includes/utility.inc
index 023ecb0..3a94fa8 100644
--- a/core/includes/utility.inc
+++ b/core/includes/utility.inc
@@ -45,15 +45,6 @@ function drupal_rebuild() {
   PhpStorageFactory::get('service_container')->deleteAll();
   PhpStorageFactory::get('twig')->deleteAll();
 
-  // Disable the page cache.
-  drupal_page_is_cacheable(FALSE);
-
-  // Bootstrap up to where caches exist and clear them.
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
-  foreach (Cache::getBins() as $bin) {
-    $bin->deleteAll();
-  }
-
   drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
   drupal_flush_all_caches();
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 8e4596f..c2321c2 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -7,8 +7,12 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\PageCache\InternalPageCacheInterface;
+use Drupal\Core\PageCache\PageCachePolicyInterface;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -22,18 +26,49 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   /**
    * The LanguageManager object for retrieving the correct language code.
    *
-   * @var LanguageManager
+   * @var \Drupal\Core\Language\LanguageManager
    */
   protected $languageManager;
 
   /**
+   * The PageCachePolicy object necessary to decide whether a page is cacheable.
+   *
+   * @var \Drupal\Core\PageCache\PageCachePolicyInterface
+   */
+  protected $pageCachePolicy;
+
+  /**
+   * The InternalPageCache object used to store cached pages.
+   *
+   * @var \Drupal\Core\PageCache\InternalPageCacheInterface
+   */
+  protected $internalPageCache;
+
+  /**
+   * A config object for the system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
    * Constructs a FinishResponseSubscriber object.
    *
-   * @param LanguageManager $language_manager
-   *  The LanguageManager object for retrieving the correct language code.
+   * @param \Drupal\Core\Language\LanguageManager $language_manager
+   *   The LanguageManager object for retrieving the correct language code.
+   * @param \Drupal\Core\PageCache\PageCachePolicyInterface $page_cache_policy
+   *   The PageCachePolicy object necessary to decide whether a page is
+   *   cacheable.
+   * @param \Drupal\Core\PageCache\InternalPageCacheInterface $internal_page_cache
+   *   The InternalPageCache object used to store cached pages.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   A config factory for retrieving required config objects.
    */
-  public function __construct(LanguageManager $language_manager) {
+  public function __construct(LanguageManager $language_manager, PageCachePolicyInterface $page_cache_policy, InternalPageCacheInterface $internal_page_cache, ConfigFactory $config_factory) {
     $this->languageManager = $language_manager;
+    $this->pageCachePolicy = $page_cache_policy;
+    $this->internalPageCache = $internal_page_cache;
+    $this->config = $config_factory->get('system.performance');
   }
 
   /**
@@ -100,9 +135,10 @@ public function onRespond(FilterResponseEvent $event) {
       $response->headers->set($name, $value, FALSE);
     }
 
-    $max_age = \Drupal::config('system.performance')->get('cache.page.max_age');
-    if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) {
-      drupal_serve_page_from_cache($cache, $response, $request);
+    $max_age = $this->config->get('cache.page.max_age');
+    $this->pageCachePolicy->applyToResponse($response, $request);
+    if ($max_age > 0 && $this->pageCachePolicy->isCacheable()) {
+      $this->internalPageCache->record($response, $request);
     }
     else {
       $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
diff --git a/core/lib/Drupal/Core/PageCache/DefaultPageCachePolicy.php b/core/lib/Drupal/Core/PageCache/DefaultPageCachePolicy.php
new file mode 100644
index 0000000..d14b735
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DefaultPageCachePolicy.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\PageCache\DefaultPageCachePolicy
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Page caching service based on the internal cache.
+ */
+class DefaultPageCachePolicy implements PageCachePolicyInterface {
+  /**
+   * @var bool
+   */
+  protected $excluded;
+
+  /**
+   * @var bool
+   */
+  protected $canceled;
+
+  /**
+   * Prevent that a page gets saved to the cache.
+   */
+  public function cancel() {
+    $this->canceled = TRUE;
+  }
+
+  /**
+   * Returns TRUE when page caching is not possible due to request properties.
+   */
+  public function isExcluded() {
+    return !empty($this->excluded);
+  }
+
+  /**
+   * Returns TRUE when page caching was canceled midways throug a page build.
+   */
+  public function isCanceled() {
+    return !empty($this->canceled);
+  }
+
+  /**
+   * Return TRUE when the page caching is allowed for the current request.
+   */
+  public function isCacheable() {
+    return !$this->isExcluded() && !$this->isCanceled();
+  }
+
+  /**
+   * Apply the default caching policy before a request is handled.
+   */
+  public function applyToRequest(Request $request) {
+    $this->excludeUncacheableHTTPMethod($request);
+    $this->excludeCLIRequest($request);
+    $this->excludeOpenSession($request);
+  }
+
+  /**
+   * Apply page caching possibly after a response was built.
+   */
+  public function applyToResponse(Response $response, Request $request) {
+  }
+
+  /**
+   * Only allow GET and HEAD requests.
+   */
+  protected function excludeUncacheableHTTPMethod(Request $request) {
+    if ($_SERVER['REQUEST_METHOD'] != 'GET' && $_SERVER['REQUEST_METHOD'] != 'HEAD') {
+      $this->excluded = TRUE;
+    }
+  }
+
+  /**
+   * Exclude page requests when drupal was invoked by the command line.
+   */
+  protected function excludeCLIRequest(Request $request) {
+    if (drupal_is_cli()) {
+      $this->excluded = TRUE;
+    }
+  }
+
+  /**
+   * Exclude page requests of users with an open session.
+   *
+   * 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.
+   */
+  protected function excludeOpenSession(Request $request) {
+    if ($request->cookies->has(session_name())) {
+      $this->excluded = TRUE;
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/InternalPageCache.php b/core/lib/Drupal/Core/PageCache/InternalPageCache.php
new file mode 100644
index 0000000..9987f97
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/InternalPageCache.php
@@ -0,0 +1,288 @@
+<?php
+/**
+ * @file
+ * Definition of Drupal\Core\PageCache\InternalPageCache.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Page caching service based on the internal cache.
+ */
+class InternalPageCache implements InternalPageCacheInterface {
+  /**
+   * Cache backend for the page cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Content negotiation service.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * A config object for the system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+
+  /**
+   * The settings instance.
+   *
+   * @var \Drupal\Component\Utility\Settings
+   */
+  protected $settings;
+
+  /**
+   * Constructs a new InternalPageCache object.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend to use.
+   * @param \Drupal\Core\ContentNegotiation $negotiation
+   *   The content negotiation service.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The configuration factory service.
+   * @param \Drupal\Component\Utility\Settings $settings
+   *   The settings instance.
+   */
+  public function __construct(CacheBackendInterface $cache, ContentNegotiation $negotiation, ConfigFactory $config_factory, Settings $settings) {
+    $this->cache = $cache;
+    $this->negotiation = $negotiation;
+    $this->config = $config_factory->get('system.performance');
+    $this->settings = $settings;
+  }
+
+  /**
+   * Attempt to deliver the given request from the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return bool
+   *   TRUE when the page was delivered from the cache.
+   */
+  public function deliver(Request $request) {
+    // Get the page from the cache.
+    $cache = $this->cache->get($this->getCid($request));
+    if (is_object($cache)) {
+      $response = new Response();
+      $response->headers->set('X-Drupal-Cache', 'HIT');
+      date_default_timezone_set(drupal_get_user_timezone());
+
+      $this->populateResponse($cache, $response, $request);
+
+      $response->prepare($request);
+      $response->send();
+      return TRUE;
+    }
+    else {
+      drupal_add_http_header('X-Drupal-Cache', 'MISS');
+      return FALSE;
+    }
+  }
+
+  /**
+   * Save a built response in the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   */
+  public function record(Response $response, Request $request) {
+    $cache = $this->set($response, $request);
+    $this->populateResponse($cache, $response, $request);
+  }
+
+  /**
+   * Gets the page cache cid for this request.
+   *
+   * @todo Move this into a dedicated service.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return string
+   *   The cid for this request.
+   */
+  protected function getCid(Request $request) {
+    $cid_parts = array(
+      $request->getUri(),
+      $this->negotiation->getContentType($request),
+    );
+    return sha1(implode(':', $cid_parts));
+  }
+
+  /**
+   * Sets HTTP headers in preparation for a cached page response.
+   *
+   * The headers allow as much as possible in proxies and browsers without any
+   * particular knowledge about the pages. Modules can override these headers
+   * using drupal_add_http_header().
+   *
+   * If the request is conditional (using If-Modified-Since and If-None-Match),
+   * and the conditions match those currently in the cache, a 304 Not Modified
+   * response is sent.
+   */
+  protected function populateResponse(\stdClass $cache, Response $response, Request $request) {
+    // First half: we must determine if we should be returning a 304.
+
+    // Negotiate whether to use compression.
+    $page_compression = !empty($cache->data['page_compressed']) && extension_loaded('zlib');
+    $return_compressed = $page_compression && $request->server->has('HTTP_ACCEPT_ENCODING') && strpos($request->server->get('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE;
+
+    // Get headers. Keys are lower-case.
+    $boot_headers = drupal_get_http_header();
+
+    foreach ($cache->data['headers'] as $name => $value) {
+      // In the case of a 304 response, certain headers must be sent, and the
+      // remaining may not (see RFC 2616, section 10.3.5).
+      $name_lower = strtolower($name);
+      if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
+        $response->headers->set($name, $value);
+        unset($cache->data['headers'][$name]);
+      }
+    }
+
+    // If the client sent a session cookie, a cached copy will only be served
+    // to that one particular client due to Vary: Cookie. Thus, do not set
+    // max-age > 0, allowing the page to be cached by external proxies, when a
+    // session cookie is present unless the Vary header has been replaced.
+    $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $this->config->get('cache.page.max_age') : 0;
+    $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
+
+    // Entity tag should change if the output changes.
+    $response->setEtag($cache->created);
+
+    // See if the client has provided the required HTTP headers.
+    $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
+    $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
+
+    if ($if_modified_since && $if_none_match
+      && $if_none_match == $response->headers->get('etag') // etag must match
+      && $if_modified_since == $cache->created) {  // if-modified-since must match
+        $response->setStatusCode(304);
+        return;
+      }
+
+    // Second half: we're not returning a 304, so put in other headers.
+
+    // Send the remaining headers.
+    foreach ($cache->data['headers'] as $name => $value) {
+      $response->headers->set($name, $value);
+      drupal_add_http_header($name, $value);
+    }
+
+    $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
+
+    // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
+    // by sending an Expires date in the past. HTTP/1.1 clients ignores the
+    // Expires header if a Cache-Control: max-age= directive is specified (see RFC
+    // 2616, section 14.9.3).
+    if (!$response->getExpires()) {
+      $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
+    }
+
+    // Allow HTTP proxies to cache pages for anonymous users without a session
+    // cookie. The Vary header is used to indicates the set of request-header
+    // fields that fully determines whether a cache is permitted to use the
+    // response to reply to a subsequent request for a given URL without
+    // revalidation.
+    if (!isset($boot_headers['vary']) && !$this->settings->get('omit_vary_cookie')) {
+      $response->setVary('Cookie', FALSE);
+    }
+
+    if ($page_compression) {
+      $response->setVary('accept-encoding', FALSE);
+      // If page_compression is enabled, the cache contains gzipped data.
+      if ($return_compressed) {
+        // $cache->data['body'] is already gzip'ed, so make sure
+        // zlib.output_compression does not compress it once more.
+        ini_set('zlib.output_compression', '0');
+        $response->headers->set('content-encoding', 'gzip');
+      }
+      else {
+        // The client does not support compression, so unzip the data in the
+        // cache. Strip the gzip header and run uncompress.
+        $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
+      }
+    }
+
+    $response->setContent($cache->data['body']);
+  }
+
+  /**
+   * Stores the current page in the cache.
+   *
+   * If page_compression is enabled, a gzipped version of the page is stored in
+   * the cache to avoid compressing the output on each request. The cache entry
+   * is unzipped in the relatively rare event that the page is requested by a
+   * client without gzip support.
+   *
+   * Page compression requires the PHP zlib extension
+   * (http://php.net/manual/ref.zlib.php).
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return \stdObject|null
+   *   The cached object or NULL if the page cache was not set.
+   *
+   * @see drupal_page_header()
+   */
+  protected function set(Response $response, Request $request) {
+    // Check if the current page may be compressed.
+    $page_compressed = $this->config->get('response.gzip') && extension_loaded('zlib');
+
+    $cache = (object) array(
+      'cid' => $this->getCid($request),
+      'data' => array(
+        'body' => $response->getContent(),
+        'headers' => array(),
+        // We need to store whether page was compressed or not,
+        // because by the time it is read, the configuration might change.
+        'page_compressed' => $page_compressed,
+      ),
+      'tags' => array('content' => TRUE) + drupal_cache_tags_page_get(),
+      'expire' => CacheBackendInterface::CACHE_PERMANENT,
+      'created' => REQUEST_TIME,
+    );
+
+    $cache->data['headers'] = $response->headers->all();
+
+    // Hack: exclude the x-drupal-cache header; it may make it in here because
+    // of awkwardness in how we defer sending it over in _drupal_page_get_cache.
+    if (isset($cache->data['headers']['x-drupal-cache'])) {
+      unset($cache->data['headers']['x-drupal-cache']);
+    }
+
+    // Use the actual timestamp from an Expires header, if available.
+    if ($date = $response->getExpires()) {
+      $date = DrupalDateTime::createFromDateTime($date);
+      $cache->expire = $date->getTimestamp();
+    }
+
+    if ($cache->data['body']) {
+      if ($page_compressed) {
+        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
+      }
+      $this->cache->set($cache->cid, $cache->data, $cache->expire, $cache->tags);
+    }
+    return $cache;
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/InternalPageCacheInterface.php b/core/lib/Drupal/Core/PageCache/InternalPageCacheInterface.php
new file mode 100644
index 0000000..90169e4
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/InternalPageCacheInterface.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * Definition of Drupal\Core\PageCache\InternalPageCacheInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Interface for page caching services based on the internal cache.
+ */
+interface InternalPageCacheInterface {
+
+  /**
+   * Attempt to deliver the given request from the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return bool
+   *   TRUE when the page was delivered from the cache.
+   */
+  public function deliver(Request $request);
+
+  /**
+   * Save a built response in the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   */
+  public function record(Response $response, Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCachePolicyInterface.php b/core/lib/Drupal/Core/PageCache/PageCachePolicyInterface.php
new file mode 100644
index 0000000..a9834b3
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCachePolicyInterface.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\PageCache\PageCachePolicy
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines the interface for page caching policy implementations.
+ *
+ * During a request the page cache policy is applied exactly twice. The first
+ * time just before attempting to deliver a cached page and the second time
+ * before a page is saved to the page cache.
+ *
+ * Throughout the request, client code may also cancel page caching. This is
+ * necessary for example when a page contains messages.
+ *
+ * If page caching has been disabled either by the policy rules or when third
+ * party code canceled the caching, the markup will not be saved to the page
+ * cache.
+ */
+interface PageCachePolicyInterface {
+  /**
+   * Prevent that a page gets saved to the cache.
+   */
+  public function cancel();
+
+  /**
+   * Returns TRUE when page caching was disabled by the policy rules.
+   */
+  public function isExcluded();
+
+  /**
+   * Returns TRUE when page caching was canceled midways througout a page build.
+   */
+  public function isCanceled();
+
+  /**
+   * Return TRUE when the page caching is allowed for the current request.
+   */
+  public function isCacheable();
+
+  /**
+   * Apply the caching policy rules before a request is handled.
+   */
+  public function applyToRequest(Request $request);
+
+  /**
+   * Apply page caching policy rules after a response was built.
+   */
+  public function applyToResponse(Response $response, Request $request);
+}
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPageCachePolicy.php b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPageCachePolicy.php
new file mode 100644
index 0000000..c48d47e
--- /dev/null
+++ b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPageCachePolicy.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\toolbar\PageCache\ToolbarPageCachePolicy
+ */
+
+namespace Drupal\toolbar\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\PageCache\DefaultPageCachePolicy;
+
+/**
+ * Page caching service based on the internal cache.
+ */
+class ToolbarPageCachePolicy extends DefaultPageCachePolicy {
+  /**
+   * Do *not* exclude users with an open session.
+   */
+  protected function excludeOpenSession(Request $request) {
+    // Intentionally left blank.
+  }
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index f35d4ae..cbe05e8 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -114,28 +114,19 @@ function toolbar_element_info() {
  */
 function _toolbar_initialize_page_cache() {
   $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
-  drupal_page_is_cacheable(TRUE);
+
+  // Replace page cache policy with our own implementation.
+  $toolbar_page_cache_policy = \Drupal::service('toolbar.page_cache.policy');
+  \Drupal::getContainer()->set('page_cache.policy', $toolbar_page_cache_policy);
 
   // If we have a cache, serve it.
-  // @see _drupal_bootstrap_page_cache()
   $request = \Drupal::request();
-  $cache = drupal_page_get_cache($request);
-  if (is_object($cache)) {
-    $response = new Response();
-    $response->headers->set('X-Drupal-Cache', 'HIT');
-    date_default_timezone_set(drupal_get_user_timezone());
-
-    drupal_serve_page_from_cache($cache, $response, $request);
-
-    $response->prepare($request);
-    $response->send();
-    // We are done.
-    exit;
+  $toolbar_page_cache_policy->applyToRequest($request);
+  // Attempt to serve the page from the cache.
+  if (\Drupal::service('page_cache.internal')->deliver($request)) {
+    exit();
   }
 
-  // Otherwise, create a new page response (that will be cached).
-  drupal_add_http_header('X-Drupal-Cache', 'MISS');
-
   // 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
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f26968..0cce863 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -6,3 +6,5 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [toolbar]
+  toolbar.page_cache.policy:
+    class: Drupal\toolbar\PageCache\ToolbarPageCachePolicy
