diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 6541350..31a1440 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\RuntimeException as DependencyInjectionRuntimeException; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Drupal\Core\Language\Language; use Drupal\Core\Lock\DatabaseLockBackend; use Drupal\Core\Lock\LockBackendInterface; @@ -1067,6 +1068,8 @@ function drupal_load($type, $name) { * reason phrase, e.g. "404 Not Found". * @param $append * Whether to append the value to an existing header or to replace it. + * + * @deprecated */ function drupal_add_http_header($name, $value, $append = FALSE) { // The headers as name/value pairs. @@ -1086,7 +1089,6 @@ function drupal_add_http_header($name, $value, $append = FALSE) { else { $headers[$name_lower] = $value; } - drupal_send_headers(array($name => $headers[$name_lower]), TRUE); } /** @@ -1099,6 +1101,8 @@ function drupal_add_http_header($name, $value, $append = FALSE) { * @return * A string containing the header value, or FALSE if the header has been set, * or NULL if the header has not been set. + * + * @deprecated */ function drupal_get_http_header($name = NULL) { $headers = &drupal_static('drupal_http_headers', array()); @@ -1115,7 +1119,9 @@ function drupal_get_http_header($name = NULL) { * Sets the preferred name for the HTTP header. * * Header names are case-insensitive, but for maximum compatibility they should - * follow "common form" (see RFC 2617, section 4.2). + * follow "common form" (see RFC 2616, section 4.2). + * + * @deprecated */ function _drupal_set_preferred_header_name($name = NULL) { static $header_names = array(); @@ -1137,6 +1143,8 @@ function _drupal_set_preferred_header_name($name = NULL) { * @param bool $only_default * (optional) If TRUE and headers have already been sent, send only the * specified headers. + * + * @deprecated */ function drupal_send_headers($default_headers = array(), $only_default = FALSE) { $headers_sent = &drupal_static(__FUNCTION__, FALSE); @@ -1191,6 +1199,8 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) * identical. * * @see drupal_page_set_cache() + * + * @deprecated */ function drupal_page_header() { $headers_sent = &drupal_static(__FUNCTION__, FALSE); @@ -1219,9 +1229,11 @@ function drupal_page_header() { * and the conditions match those currently in the cache, a 304 Not Modified * response is sent. */ -function drupal_serve_page_from_cache(stdClass $cache) { +function drupal_serve_page_from_cache(stdClass $cache, Response $response) { $config = config('system.performance'); + // First half: we must determine if we should be returning a 304. + // Negotiate whether to use compression. $page_compression = $config->get('response.gzip') && extension_loaded('zlib'); $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE; @@ -1229,57 +1241,54 @@ function drupal_serve_page_from_cache(stdClass $cache) { // Get headers. Keys are lower-case. $boot_headers = drupal_get_http_header(); - // Headers generated in this function, that may be replaced or unset using - // drupal_add_http_headers(). Keys are mixed-case. - $default_headers = array(); - 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])) { - drupal_add_http_header($name, $value); + $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 = !isset($_COOKIE[session_name()]) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0; - $default_headers['Cache-Control'] = 'public, max-age=' . $max_age; - // Entity tag should change if the output changes. - $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"'; - header('Etag: ' . $etag); + $response->setEtag($cache->created); // See if the client has provided the required HTTP headers. $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE; $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE; if ($if_modified_since && $if_none_match - && $if_none_match == $etag // etag must match + && $if_none_match == $response->headers->get('etag') // etag must match && $if_modified_since == $cache->created) { // if-modified-since must match - header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); - drupal_send_headers($default_headers); + $response->setStatusCode(304); return; } + // Second half: we're not returning a 304, so put in other headers. + + // 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 = !isset($_COOKIE[session_name()]) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0; + $response->headers->set('Cache-Control', 'public, max-age=' . $max_age); + // Send the remaining headers. foreach ($cache->data['headers'] as $name => $value) { + $response->headers->set($name, $value); drupal_add_http_header($name, $value); } - $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created); + $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). - $default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'; - - drupal_send_headers($default_headers); + 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 @@ -1287,17 +1296,17 @@ function drupal_serve_page_from_cache(stdClass $cache) { // response to reply to a subsequent request for a given URL without // revalidation. if (!isset($boot_headers['vary']) && !settings()->get('omit_vary_cookie')) { - header('Vary: Cookie'); + $response->setVary('cookie', FALSE); } if ($page_compression) { - header('Vary: Accept-Encoding', FALSE); + $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'); - header('Content-Encoding: gzip'); + $response->headers->set('content-encoding', 'gzip'); } else { // The client does not support compression, so unzip the data in the @@ -1306,8 +1315,7 @@ function drupal_serve_page_from_cache(stdClass $cache) { } } - // Print the page. - print $cache->data['body']; + $response->setContent($cache->data['body']); } /** @@ -2078,18 +2086,23 @@ function _drupal_bootstrap_page_cache() { $cache = drupal_page_get_cache(); // If there is a cached page, display it. if (is_object($cache)) { - header('X-Drupal-Cache: HIT'); + $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); + drupal_serve_page_from_cache($cache, $response); + // We are done. + $request = drupal_container()->get('request'); + $response->prepare($request); + $response->send(); exit; } else { - header('X-Drupal-Cache: MISS'); + drupal_add_http_header('X-Drupal-Cache', 'MISS'); } } } diff --git a/core/includes/common.inc b/core/includes/common.inc index 4f72021..7b17fc3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5,6 +5,8 @@ use Drupal\Core\Language\Language; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Yaml\Parser; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; use Drupal\Component\PhpStorage\PhpStorageFactory; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; @@ -4650,15 +4652,15 @@ function _drupal_bootstrap_full($skip = FALSE) { * * @see drupal_page_header() */ -function drupal_page_set_cache($body) { +function drupal_page_set_cache(Response $response, Request $request) { global $base_root; if (drupal_page_is_cacheable()) { $cache = (object) array( - 'cid' => $base_root . request_uri(), + 'cid' => $base_root . $request->getRequestUri(), 'data' => array( - 'path' => current_path(), - 'body' => $body, + 'path' => $request->getQueryString(), + 'body' => $response->getContent(), 'title' => drupal_get_title(), 'headers' => array(), ), @@ -4667,16 +4669,18 @@ function drupal_page_set_cache($body) { 'created' => REQUEST_TIME, ); - // Restore preferred header names based on the lower-case names returned - // by drupal_get_http_header(). - $header_names = _drupal_set_preferred_header_name(); - foreach (drupal_get_http_header() as $name_lower => $value) { - $cache->data['headers'][$header_names[$name_lower]] = $value; - if ($name_lower == 'expires') { - // Use the actual timestamp from an Expires header if available. - $date = new DrupalDateTime($value); - $cache->expire = $date->getTimestamp(); - } + $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 = new DrupalDateTime($date); + $cache->expire = $date->getTimestamp(); } if ($cache->data['body']) { diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 163d5c8..1454707 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -47,6 +47,7 @@ public function onRespond(FilterResponseEvent $event) { return; } + $request = $event->getRequest(); $response = $event->getResponse(); // Set the X-UA-Compatible HTTP header to force IE to use the most recent @@ -60,6 +61,7 @@ public function onRespond(FilterResponseEvent $event) { // since the page is in fact being regenerated right now. // @todo Remove this and use a more intelligent default so that HTTP // caching can function properly. + // @todo use $response->setLastModified() $response->headers->set('Last-Modified', gmdate(DATE_RFC1123, REQUEST_TIME)); // Also give each page a unique ETag. This will force clients to include @@ -82,25 +84,31 @@ public function onRespond(FilterResponseEvent $event) { // identical. // @todo Remove this line as no longer necessary per // http://drupal.org/node/1573064 - $response->headers->set('ETag', '"' . REQUEST_TIME . '"'); + $response->setEtag(REQUEST_TIME); // Authenticated users are always given a 'no-cache' header, and will fetch // a fresh page on every request. This prevents authenticated users from // seeing locally cached pages. // @todo Revisit whether or not this is still appropriate now that the - // Response object does its own cache control procesisng and we intend to + // Response object does its own cache control processing and we intend to // use partial page caching more extensively. // Commit the user session, if needed. drupal_session_commit(); + + // Attach globally-declared headers to the response object so that Symfony + // can send them for us correctly. + // @todo remove this once we have removed all drupal_add_http_header() calls + $headers = drupal_get_http_header(); + foreach ($headers as $name => $value) { + $response->headers->set($name, $value, FALSE); + } + $max_age = config('system.performance')->get('cache.page.max_age'); - if ($max_age > 0 && ($cache = drupal_page_set_cache($response->getContent()))) { - drupal_serve_page_from_cache($cache); - // drupal_serve_page_from_cache() already printed the response. - $response->setContent(''); - $response->headers->remove('cache-control'); + if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) { + drupal_serve_page_from_cache($cache, $response); } else { - $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT'); + $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT')); $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0'); } } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index a75ade4..5e362f1 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -54,6 +54,13 @@ protected $headers; /** + * Indicates that headers should be dumped if verbose output is enabled. + * + * @var bool + */ + protected $dumpHeaders = FALSE; + + /** * The content of the page currently loaded in the internal browser. * * @var string @@ -1191,9 +1198,15 @@ protected function drupalGet($path, array $options = array(), array $headers = a if ($new = $this->checkForMetaRefresh()) { $out = $new; } - $this->verbose('GET request to: ' . $path . - '
Ending URL: ' . $this->getUrl() . - '
' . $out); + + $verbose = 'GET request to: ' . $path . + '
Ending URL: ' . $this->getUrl(); + if ($this->dumpHeaders) { + $verbose .= '
Headers:
' . check_plain(var_export(array_map('trim', $this->headers), TRUE)) . '
'; + } + $verbose .= '
' . $out; + + $this->verbose($verbose); return $out; } @@ -1633,6 +1646,13 @@ protected function drupalHead($path, array $options = array(), array $headers = $options['absolute'] = TRUE; $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers)); $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. + + if ($this->dumpHeaders) { + $this->verbose('GET request to: ' . $path . + '
Ending URL: ' . $this->getUrl() . + '
Headers:
' . check_plain(var_export(array_map('trim', $this->headers), TRUE)) . '
'); + } + return $out; } diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php index 8ee5bba..5bab747 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php @@ -21,6 +21,8 @@ class PageCacheTest extends WebTestBase { */ public static $modules = array('test_page_test', 'system_test'); + protected $dumpHeaders = TRUE; + public static function getInfo() { return array( 'name' => 'Page cache test', @@ -90,21 +92,25 @@ function testPageCache() { $config = config('system.performance'); $config->set('cache.page.use_internal', 1); $config->set('cache.page.max_age', 300); + $config->set('response.gzip', 1); $config->save(); // Fill the cache. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.'); - $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', 'Vary header was sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=300', 'Cache-Control header was sent.'); + $this->assertEqual($this->drupalGetHeader('Vary'), 'cookie,accept-encoding', 'Vary header was sent.'); + // Symfony's Response logic determines a specific order for the subvalues + // of the Cache-Control header, even if they are explicitly passed in to + // the response header bag in a different order. + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.'); $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.'); // Check cache. $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar'))); $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.'); - $this->assertEqual($this->drupalGetHeader('Vary'), 'Cookie,Accept-Encoding', 'Vary: Cookie header was sent.'); - $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'public, max-age=300', 'Cache-Control header was sent.'); + $this->assertEqual($this->drupalGetHeader('Vary'), 'cookie,accept-encoding', 'Vary: Cookie header was sent.'); + $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.'); $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.'); $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.'); @@ -112,7 +118,7 @@ function testPageCache() { $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Expires', 'value' => 'Fri, 19 Nov 2008 05:00:00 GMT'))); $this->assertEqual($this->drupalGetHeader('Expires'), 'Fri, 19 Nov 2008 05:00:00 GMT', 'Default header was replaced.'); $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Vary', 'value' => 'User-Agent'))); - $this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,Accept-Encoding', 'Default header was replaced.'); + $this->assertEqual($this->drupalGetHeader('Vary'), 'User-Agent,accept-encoding', 'Default header was replaced.'); // Check that authenticated users bypass the cache. $user = $this->drupalCreateUser(); @@ -146,6 +152,7 @@ function testPageCompression() { $config = config('system.performance'); $config->set('cache.page.use_internal', 1); $config->set('cache.page.max_age', 300); + $config->set('response.gzip', 1); $config->save(); // Fill the cache and verify that output is compressed. diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php index d1a601a..f96b13e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php @@ -18,6 +18,8 @@ class SessionTest extends WebTestBase { */ public static $modules = array('session_test'); + protected $dumpHeaders = TRUE; + public static function getInfo() { return array( 'name' => 'Session tests', diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index c9f0c22..ea98db0 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Drupal\Core\Template\Attribute; use Drupal\Component\Utility\Crypt; +use Symfony\Component\HttpFoundation\Response; /** * Implements hook_help(). @@ -129,19 +130,23 @@ function _toolbar_initialize_page_cache() { // @see _drupal_bootstrap_page_cache() $cache = drupal_page_get_cache(); if (is_object($cache)) { - header('X-Drupal-Cache: HIT'); + $response = new Response(); + $response->headers->set('X-Drupal-Cache', 'HIT'); // Restore the metadata cached with the page. $_GET['q'] = $cache->data['path']; date_default_timezone_set(drupal_get_user_timezone()); - drupal_serve_page_from_cache($cache); + drupal_serve_page_from_cache($cache, $response); + $request = drupal_container()->get('request'); + $response->prepare($request); + $response->send(); // We are done. exit; } // Otherwise, create a new page response (that will be cached). - header('X-Drupal-Cache: MISS'); + 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