diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 9dd3f56..e7108e7 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -17,6 +17,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; @@ -1068,6 +1069,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 Header handling is being shifted to a Symfony response object. */ function drupal_add_http_header($name, $value, $append = FALSE) { // The headers as name/value pairs. @@ -1087,7 +1090,6 @@ function drupal_add_http_header($name, $value, $append = FALSE) { else { $headers[$name_lower] = $value; } - drupal_send_headers(array($name => $headers[$name_lower]), TRUE); } /** @@ -1100,6 +1102,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 Header handling is being shifted to a Symfony response object. */ function drupal_get_http_header($name = NULL) { $headers = &drupal_static('drupal_http_headers', array()); @@ -1116,7 +1120,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 Header handling is being shifted to a Symfony response object. */ function _drupal_set_preferred_header_name($name = NULL) { static $header_names = array(); @@ -1138,6 +1144,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 Header handling is being shifted to a Symfony response object. */ function drupal_send_headers($default_headers = array(), $only_default = FALSE) { $headers_sent = &drupal_static(__FUNCTION__, FALSE); @@ -1192,6 +1200,8 @@ function drupal_send_headers($default_headers = array(), $only_default = FALSE) * identical. * * @see drupal_page_set_cache() + * + * @deprecated Header handling is being shifted to a Symfony response object. */ function drupal_page_header() { $headers_sent = &drupal_static(__FUNCTION__, FALSE); @@ -1220,9 +1230,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; @@ -1230,57 +1242,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 @@ -1288,17 +1297,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 @@ -1307,8 +1316,7 @@ function drupal_serve_page_from_cache(stdClass $cache) { } } - // Print the page. - print $cache->data['body']; + $response->setContent($cache->data['body']); } /** @@ -2075,18 +2083,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 84cfa94..80027d5 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -8,6 +8,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; @@ -4405,15 +4407,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(), ), @@ -4422,16 +4424,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/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php index 8ee5bba..62244e5 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,13 +92,17 @@ 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.'); + // 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.'); @@ -104,7 +110,7 @@ function testPageCache() { $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('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.'); @@ -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/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php index 2bb96b9..b0e255e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Update/UpdateScriptTest.php @@ -21,6 +21,8 @@ class UpdateScriptTest extends WebTestBase { */ public static $modules = array('update_script_test', 'dblog'); + protected $dumpHeaders = TRUE; + private $update_url; private $update_user; 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 diff --git a/core/update.php b/core/update.php index cee23c4..6e85b0f 100644 --- a/core/update.php +++ b/core/update.php @@ -302,6 +302,7 @@ function update_info_page() { */ function update_access_denied_page() { drupal_add_http_header('Status', '403 Forbidden'); + header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); watchdog('access denied', 'update.php', NULL, WATCHDOG_WARNING); drupal_set_title('Access denied'); return '

Access denied. You are not authorized to access this page. Log in using either an account with the administer software updates permission or the site maintenance account (the account you created during installation). If you cannot log in, you will have to edit settings.php to bypass this access check. To do this: