diff --git a/core/includes/common.inc b/core/includes/common.inc index 349449a..8076ba8 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -4195,7 +4195,7 @@ function drupal_flush_all_caches() { */ function _drupal_flush_css_js() { // The timestamp is converted to base 36 in order to make it more compact. - Drupal::state()->set('system.css_js_query_string', base_convert(REQUEST_TIME, 10, 36)); + Drupal::state()->set('system.drupal_asset_query_string', base_convert(REQUEST_TIME, 10, 36)); } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 2b06429..0822c43 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -913,30 +913,47 @@ function theme_get_setting($setting_name, $theme = NULL) { } } + // A dummy query-string is added to filenames, to gain control over + // browser-caching. The string changes on every update or full cache + // flush, forcing browsers to load a new copy of the files, as the + // URL changed. + try { + $query_string = \Drupal::state()->get('system.drupal_asset_query_string') ?: '0'; + } + catch (Exception $e) { + $query_string = '0'; + } + // Generate the path to the logo image. if ($cache[$theme]->get('features.logo')) { - $logo_path = $cache[$theme]->get('logo.path'); if ($cache[$theme]->get('logo.use_default')) { - $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.png')); + if (!file_exists($logo_path = $theme_object->getPath() . '/logo.png')) { + $logo_path = 'core/misc/druplicon.png'; + } } - elseif ($logo_path) { - $cache[$theme]->set('logo.url', file_create_url($logo_path)); + else { + $logo_path = $cache[$theme]->get('logo.path'); + } + if ($logo_path) { + $query_string_separator = (strpos($logo_path, '?') !== FALSE) ? '&' : '?'; + $cache[$theme]->set('logo.url', file_create_url($logo_path) . $query_string_separator . $query_string); } } // Generate the path to the favicon. if ($cache[$theme]->get('features.favicon')) { - $favicon_path = $cache[$theme]->get('favicon.path'); if ($cache[$theme]->get('favicon.use_default')) { - if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) { - $cache[$theme]->set('favicon.url', file_create_url($favicon)); - } - else { - $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico')); + if (!file_exists($favicon_path = $theme_object->getPath() . '/favicon.ico')) { + $favicon_path = 'core/misc/favicon.ico'; } } - elseif ($favicon_path) { - $cache[$theme]->set('favicon.url', file_create_url($favicon_path)); + else { + $favicon_path = $cache[$theme]->get('favicon.path'); + } + if ($favicon_path) { + $query_string_separator = (strpos($favicon_path, '?') !== FALSE) ? '&' : '?'; + $favicon_url = file_create_url($favicon_path) . $query_string_separator . $query_string; + $cache[$theme]->set('favicon.url', $favicon_url); } else { $cache[$theme]->set('features.favicon', FALSE); diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php index 0d482fb..ef2d875 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php @@ -41,7 +41,7 @@ public function render(array $css_assets) { // browser-caching. The string changes on every update or full cache // flush, forcing browsers to load a new copy of the files, as the // URL changed. - $query_string = $this->state->get('system.css_js_query_string') ?: '0'; + $query_string = $this->state->get('system.drupal_asset_query_string') ?: '0'; // Defaults for LINK and STYLE elements. $link_element_defaults = array( diff --git a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php index 807179f..c6db7a0 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionRenderer.php @@ -43,7 +43,7 @@ public function render(array $js_assets) { // URL changed. Files that should not be cached (see _drupal_add_js()) // get REQUEST_TIME as query-string instead, to enforce reload on every // page request. - $default_query_string = $this->state->get('system.css_js_query_string') ?: '0'; + $default_query_string = $this->state->get('system.drupal_asset_query_string') ?: '0'; // For inline JavaScript to validate as XHTML, all JavaScript containing // XHTML needs to be wrapped in CDATA. To make that backwards compatible diff --git a/core/modules/system/src/Tests/Common/CascadingStylesheetsTest.php b/core/modules/system/src/Tests/Common/CascadingStylesheetsTest.php index 97b1fc6..84f5175 100644 --- a/core/modules/system/src/Tests/Common/CascadingStylesheetsTest.php +++ b/core/modules/system/src/Tests/Common/CascadingStylesheetsTest.php @@ -77,7 +77,7 @@ function testRenderFile() { $styles = drupal_get_css(); $this->assertTrue(strpos($styles, $css) > 0, 'Rendered CSS includes the added stylesheet.'); // Verify that newlines are properly added inside style tags. - $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $css_processed = ''; $this->assertEqual(trim($styles), $css_processed, 'Rendered CSS includes newlines inside style tags for JavaScript use.'); } @@ -183,7 +183,7 @@ function testAddCssFileWithQueryString() { _drupal_add_css($css_with_query_string); $styles = drupal_get_css(); - $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $this->assertTrue(strpos($styles, $css_without_query_string . '?' . $query_string), 'Query string was appended correctly to css.'); $this->assertTrue(strpos($styles, str_replace('&', '&', $css_with_query_string)), 'Query string not escaped on a URI.'); } diff --git a/core/modules/system/src/Tests/Common/JavaScriptTest.php b/core/modules/system/src/Tests/Common/JavaScriptTest.php index f6f3da8..83b2985 100644 --- a/core/modules/system/src/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/src/Tests/Common/JavaScriptTest.php @@ -108,7 +108,7 @@ function testAddExternal() { * Tests adding JavaScript files with additional attributes. */ function testAttributes() { - $default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $default_query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $attached['#attached']['library'][] = 'core/drupal'; $attached['#attached']['js']['http://example.com/script.js'] = array( @@ -135,7 +135,7 @@ function testAggregatedAttributes() { // Enable aggregation. \Drupal::config('system.performance')->set('js.preprocess', 1)->save(); - $default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $default_query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $attached['#attached']['library'][] = 'core/drupal'; $attached['#attached']['js']['http://example.com/script.js'] = array( @@ -353,7 +353,7 @@ function testDifferentWeight() { * @see drupal_pre_render_conditional_comments() */ function testBrowserConditionalComments() { - $default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $default_query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $attached['#attached']['library'][] = 'core/drupal'; $attached['#attached']['js']['core/misc/collapse.js'] = array( @@ -390,7 +390,7 @@ function testVersionQueryString() { * Tests JavaScript grouping and aggregation. */ function testAggregation() { - $default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $default_query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; // To optimize aggregation, items with the 'every_page' option are ordered // ahead of ones without. The order of JavaScript execution must be the @@ -700,7 +700,7 @@ function testAddJsFileWithQueryString() { $js = drupal_get_path('module', 'node') . '/node.js'; _drupal_add_js($js); - $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $query_string = $this->container->get('state')->get('system.drupal_asset_query_string') ?: '0'; $scripts = drupal_get_js(); $this->assertTrue(strpos($scripts, $js . '?' . $query_string), 'Query string was appended correctly to JS.'); } diff --git a/core/modules/system/src/Tests/System/ThemeTest.php b/core/modules/system/src/Tests/System/ThemeTest.php index 38d5a94..9c3766e 100644 --- a/core/modules/system/src/Tests/System/ThemeTest.php +++ b/core/modules/system/src/Tests/System/ThemeTest.php @@ -55,31 +55,39 @@ function testThemeSettings() { $file_relative = strtr($file->uri, array('public:/' => PublicStream::basePath())); $default_theme_path = 'core/themes/stark'; + // Logos and favicons receive a query-string. + try { + $query_string = \Drupal::state()->get('system.drupal_asset_query_string') ?: '0'; + } + catch(Exception $e) { + $query_string = '0'; + } + $supported_paths = array( // Raw stream wrapper URI. $file->uri => array( 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), + 'src' => file_create_url($file->uri) . '?' . $query_string, ), // Relative path within the public filesystem. file_uri_target($file->uri) => array( 'form' => file_uri_target($file->uri), - 'src' => file_create_url($file->uri), + 'src' => file_create_url($file->uri) . '?' . $query_string, ), // Relative path to a public file. $file_relative => array( 'form' => $file_relative, - 'src' => file_create_url($file->uri), + 'src' => file_create_url($file->uri) . '?' . $query_string, ), // Relative path to an arbitrary file. 'core/misc/druplicon.png' => array( 'form' => 'core/misc/druplicon.png', - 'src' => $GLOBALS['base_url'] . '/' . 'core/misc/druplicon.png', + 'src' => $GLOBALS['base_url'] . '/' . 'core/misc/druplicon.png' . '?' . $query_string, ), // Relative path to a file in a theme. $default_theme_path . '/logo.png' => array( 'form' => $default_theme_path . '/logo.png', - 'src' => $GLOBALS['base_url'] . '/' . $default_theme_path . '/logo.png', + 'src' => $GLOBALS['base_url'] . '/' . $default_theme_path . '/logo.png' . '?' . $query_string, ), ); foreach ($supported_paths as $input => $expected) { @@ -177,7 +185,40 @@ function testThemeSettings() { ':rel' => 'home', ) ); - $this->assertEqual($elements[0]['src'], file_create_url($uploaded_filename)); + $this->assertEqual($elements[0]['src'], file_create_url($uploaded_filename) . '?' . $query_string); + } + + /** + * Test clearing the Drupal asset query-string cache. + */ + function testDrupalAssetCacheFlush() { + + // Obtain the query-string. + try { + $query_string = \Drupal::state()->get('system.drupal_asset_query_string') ?: '0'; + } + catch(Exception $e) { + $query_string = '0'; + } + + // Flush the query-string cache. + _drupal_flush_css_js(); + + // Obtain the new query-string. + try { + $new_query_string = \Drupal::state()->get('system.drupal_asset_query_string') ?: '0'; + } + catch(Exception $e) { + $this->fail('Requesting Drupal asset query-string after cache flush produced an exception.'); + } + + // Ensure the cache flush sets a non-zero query-string. + $this->assertNotIdentical($new_query_string, '0', 'Drupal asset query-string is non-zero after cache flush.'); + + // Ensure the cache flush changes the query-string from its original value. + if ($new_query_string !== '0') { + $this->assertNotIdentical($query_string, $new_query_string, 'Drupal asset query-string changes after cache flush.'); + } } /** diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php index 4184ec6..f5f8324 100644 --- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php @@ -543,7 +543,7 @@ function providerTestRender() { function testRender(array $css_assets, array $render_elements) { $this->state->expects($this->once()) ->method('get') - ->with('system.css_js_query_string') + ->with('system.drupal_asset_query_string') ->will($this->returnValue(NULL)); $this->assertSame($render_elements, $this->renderer->render($css_assets)); } @@ -554,7 +554,7 @@ function testRender(array $css_assets, array $render_elements) { function testRenderInvalidType() { $this->state->expects($this->once()) ->method('get') - ->with('system.css_js_query_string') + ->with('system.drupal_asset_query_string') ->will($this->returnValue(NULL)); $this->setExpectedException('Exception', 'Invalid CSS asset type.');