diff --git a/core/core.services.yml b/core/core.services.yml
index 4d178eb5f9..9537c21565 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1218,10 +1218,10 @@ services:
arguments: ['@current_user']
ajax_response.attachments_processor:
class: Drupal\Core\Ajax\AjaxResponseAttachmentsProcessor
- arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
+ arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager']
html_response.attachments_processor:
class: Drupal\Core\Render\HtmlResponseAttachmentsProcessor
- arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
+ arguments: ['@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager']
html_response.subscriber:
class: Drupal\Core\EventSubscriber\HtmlResponseSubscriber
tags:
@@ -1480,8 +1480,8 @@ services:
class: Drupal\Core\Asset\CssCollectionRenderer
arguments: [ '@state', '@file_url_generator' ]
asset.css.collection_optimizer:
- class: Drupal\Core\Asset\CssCollectionOptimizer
- arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@asset.css.dumper', '@state', '@file_system']
+ class: Drupal\Core\Asset\CssCollectionOptimizerLazy
+ arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@state', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager']
asset.css.optimizer:
class: Drupal\Core\Asset\CssOptimizer
arguments: ['@file_url_generator']
@@ -1494,8 +1494,8 @@ services:
class: Drupal\Core\Asset\JsCollectionRenderer
arguments: [ '@state', '@file_url_generator' ]
asset.js.collection_optimizer:
- class: Drupal\Core\Asset\JsCollectionOptimizer
- arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state', '@file_system']
+ class: Drupal\Core\Asset\JsCollectionOptimizerLazy
+ arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@state', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager']
asset.js.optimizer:
class: Drupal\Core\Asset\JsOptimizer
asset.js.collection_grouper:
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
index 4f6acbf966..054bf84ab3 100644
--- a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php
@@ -7,6 +7,7 @@
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Drupal\Core\Render\RendererInterface;
@@ -70,6 +71,13 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
*/
protected $moduleHandler;
+ /**
+ * The language manager.
+ *
+ * @var \Drupal\Core\Language\LanguageManagerInterface
+ */
+ protected $languageManager;
+
/**
* Constructs an AjaxResponseAttachmentsProcessor object.
*
@@ -87,8 +95,10 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The language manager.
*/
- public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager = NULL) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
@@ -96,6 +106,11 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
+ if (!isset($language_manager)) {
+ @trigger_error('Calling ' . __METHOD__ . '() without the $language_manager argument is deprecated in drupal:10.1.0 and will be required in drupal:11.0.0', E_USER_DEPRECATED);
+ $language_manager = \Drupal::languageManager();
+ }
+ $this->languageManager = $language_manager;
}
/**
@@ -141,8 +156,8 @@ protected function buildAttachmentsCommands(AjaxResponse $response, Request $req
$assets->setLibraries($attachments['library'] ?? [])
->setAlreadyLoadedLibraries(isset($ajax_page_state['libraries']) ? explode(',', $ajax_page_state['libraries']) : [])
->setSettings($attachments['drupalSettings'] ?? []);
- $css_assets = $this->assetResolver->getCssAssets($assets, $optimize_css);
- [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, $optimize_js);
+ $css_assets = $this->assetResolver->getCssAssets($assets, $optimize_css, $this->languageManager->getCurrentLanguage());
+ [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, $optimize_js, $this->languageManager->getCurrentLanguage());
// First, AttachedAssets::setLibraries() ensures duplicate libraries are
// removed: it converts it to a set of libraries if necessary. Second,
diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php
new file mode 100644
index 0000000000..244013fc58
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php
@@ -0,0 +1,23 @@
+dumpToUri($data, $file_extension, $uri);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dumpToUri($data, $file_extension, $uri): string {
+ $path = 'public://' . $file_extension;
// Create the CSS or JS file.
$this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);
try {
diff --git a/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php
new file mode 100644
index 0000000000..72758d817a
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php
@@ -0,0 +1,25 @@
+ NULL,
+ 'group' => NULL,
+ 'media' => NULL,
+ 'browsers' => NULL,
+ ];
+
+ $normalized['asset_group'] = array_intersect_key($group, $group_keys);
+ $normalized['asset_group']['items'] = [];
+ // Remove some keys to make the hash more stable.
+ $omit_keys = [
+ 'weight' => NULL,
+ ];
+ foreach ($group['items'] as $key => $asset) {
+ $normalized['asset_group']['items'][$key] = array_diff_key($asset, $group_keys, $omit_keys);
+ }
+ // The asset array ensures that a valid hash can only be generated via the
+ // same code base. Additionally use the hash salt to ensure that hashes are
+ // not re-usable between different installations.
+ return Crypt::hashBase64(Settings::getHashSalt() . serialize($normalized));
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php
index 6f477d19f6..f4ca58bb3f 100644
--- a/core/lib/Drupal/Core/Asset/AssetResolver.php
+++ b/core/lib/Drupal/Core/Asset/AssetResolver.php
@@ -7,6 +7,7 @@
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
@@ -109,12 +110,15 @@ protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
/**
* {@inheritdoc}
*/
- public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
+ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL) {
+ if (!isset($language)) {
+ $language = $this->languageManager->getCurrentLanguage();
+ }
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_library_info_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
- $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
+ $cid = 'css:' . $theme_info->getName() . ':' . $language->getId() . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
@@ -151,14 +155,14 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
}
// Allow modules and themes to alter the CSS assets.
- $this->moduleHandler->alter('css', $css, $assets);
- $this->themeManager->alter('css', $css, $assets);
+ $this->moduleHandler->alter('css', $css, $assets, $language);
+ $this->themeManager->alter('css', $css, $assets, $language);
// Sort CSS items, so that they appear in the correct order.
uasort($css, [static::class, 'sort']);
if ($optimize) {
- $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
+ $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css, $libraries_to_load, $language);
}
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
@@ -194,13 +198,16 @@ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
/**
* {@inheritdoc}
*/
- public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
+ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL) {
+ if (!isset($language)) {
+ $language = $this->languageManager->getCurrentLanguage();
+ }
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_library_info_alter(). Additionally add the current language to
// support translation of JavaScript files via hook_js_alter().
$libraries_to_load = $this->getLibrariesToLoad($assets);
- $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
+ $cid = 'js:' . $theme_info->getName() . ':' . $language->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
[$js_assets_header, $js_assets_footer, $settings, $settings_in_header] = $cached->data;
@@ -258,8 +265,8 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
}
// Allow modules and themes to alter the JavaScript assets.
- $this->moduleHandler->alter('js', $javascript, $assets);
- $this->themeManager->alter('js', $javascript, $assets);
+ $this->moduleHandler->alter('js', $javascript, $assets, $language);
+ $this->themeManager->alter('js', $javascript, $assets, $language);
// Sort JavaScript assets, so that they appear in the correct order.
uasort($javascript, [static::class, 'sort']);
@@ -278,8 +285,8 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
if ($optimize) {
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
- $js_assets_header = $collection_optimizer->optimize($js_assets_header);
- $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
+ $js_assets_header = $collection_optimizer->optimize($js_assets_header, $libraries_to_load);
+ $js_assets_footer = $collection_optimizer->optimize($js_assets_footer, $libraries_to_load);
}
// If the core/drupalSettings library is being loaded or is already
diff --git a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
index 66f6ec095a..9100ebd57c 100644
--- a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
+++ b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php
@@ -2,6 +2,8 @@
namespace Drupal\Core\Asset;
+use Drupal\Core\Language\LanguageInterface;
+
/**
* Resolves asset libraries into concrete CSS and JavaScript assets.
*
@@ -43,11 +45,13 @@ interface AssetResolverInterface {
* @param bool $optimize
* Whether to apply the CSS asset collection optimizer, to return an
* optimized CSS asset collection rather than an unoptimized one.
+ * @param \Drupal\Core\Language\LanguageInterface $language
+ * (optional) The interface language the assets will be rendered with.
*
* @return array
* A (possibly optimized) collection of CSS assets.
*/
- public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
+ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL);
/**
* Returns the JavaScript assets for the current response's libraries.
@@ -69,6 +73,8 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
* @param bool $optimize
* Whether to apply the JavaScript asset collection optimizer, to return
* optimized JavaScript asset collections rather than an unoptimized ones.
+ * @param \Drupal\Core\Language\LanguageInterface $language
+ * (optional) The interface language for the assets will be rendered with.
*
* @return array
* A nested array containing 2 values:
@@ -77,6 +83,6 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
* - at index one: the (possibly optimized) collection of JavaScript assets
* for the bottom of the page
*/
- public function getJsAssets(AttachedAssetsInterface $assets, $optimize);
+ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL);
}
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
index 7d47dad86e..5a913ea41d 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
@@ -2,11 +2,18 @@
namespace Drupal\Core\Asset;
+@trigger_error('The ' . __NAMESPACE__ . '\CssCollectionOptimizer is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead, use ' . __NAMESPACE__ . '\CssCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED);
+
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface;
/**
* Optimizes CSS assets.
+ *
+ * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead, use
+ * \Drupal\Core\Asset\CssCollectionOptimizerLazy.
+ *
+ * @see https://www.drupal.org/node/2888767*
*/
class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
@@ -81,7 +88,7 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim
* configurable period (@code system.performance.stale_file_threshold @endcode)
* to ensure that files referenced by a cached page will still be available.
*/
- public function optimize(array $css_assets) {
+ public function optimize(array $css_assets, array $libraries) {
// Group the assets.
$css_groups = $this->grouper->group($css_assets);
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
new file mode 100644
index 0000000000..dbf007a4fe
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
@@ -0,0 +1,180 @@
+grouper->group($css_assets);
+
+ $css_assets = [];
+ foreach ($css_groups as $order => $css_group) {
+ // We have to return a single asset, not a group of assets. It is now up
+ // to one of the pieces of code in the switch statement below to set the
+ // 'data' property to the appropriate value.
+ $css_assets[$order] = $css_group;
+
+ if ($css_group['type'] === 'file') {
+ // No preprocessing, single CSS asset: just use the existing URI.
+ if (!$css_group['preprocess']) {
+ $uri = $css_group['items'][0]['data'];
+ $css_assets[$order]['data'] = $uri;
+ }
+ else {
+ // To reproduce the full context of assets outside of the request,
+ // we must know the entire set of libraries used to generate all CSS
+ // groups, whether or not files in a group are from a particular
+ // library or not.
+ $css_assets[$order]['preprocessed'] = TRUE;
+ }
+ }
+ if ($css_group['type'] === 'external') {
+ // We don't do any aggregation and hence also no caching for external
+ // CSS assets.
+ $uri = $css_group['items'][0]['data'];
+ $css_assets[$order]['data'] = $uri;
+ }
+ }
+ // Generate a URL for each group of assets, but do not process them inline,
+ // this is done using optimizeGroup() when the asset path is requested.
+ $ajax_page_state = $this->requestStack->getCurrentRequest()->get('ajax_page_state');
+ $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [];
+ $query_args = [
+ 'language' => $this->languageManager->getCurrentLanguage()->getId(),
+ 'theme' => $this->themeManager->getActiveTheme()->getName(),
+ 'include' => implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($libraries)),
+ ];
+ if ($already_loaded) {
+ $query_args['exclude'] = implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded));
+ }
+ foreach ($css_assets as $order => $css_asset) {
+ if (!empty($css_asset['preprocessed'])) {
+ $query = ['delta' => "$order"] + $query_args;
+ $filename = 'css_' . $this->generateHash($css_asset) . '.css';
+ $uri = 'public://css/' . $filename;
+ $css_assets[$order]['data'] = $this->fileUrlGenerator->generateAbsoluteString($uri) . '?' . UrlHelper::buildQuery($query);
+ }
+ unset($css_assets[$order]['items']);
+ }
+
+ return $css_assets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAll() {
+ return $this->state->get('drupal_css_cache_files', []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteAll() {
+ $this->state->delete('drupal_css_cache_files');
+
+ $delete_stale = function ($uri) {
+ $threshold = $this->configFactory
+ ->get('system.performance')
+ ->get('stale_file_threshold');
+ // Default stale file threshold is 30 days.
+ if ($this->time->getRequestTime() - filemtime($uri) > $threshold) {
+ $this->fileSystem->delete($uri);
+ }
+ };
+ if (is_dir('public://css')) {
+ $this->fileSystem->scanDirectory('public://css', '/.*/', ['callback' => $delete_stale]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function optimizeGroup(array $group): string {
+ // Optimize each asset within the group.
+ $data = '';
+ foreach ($group['items'] as $css_asset) {
+ $data .= $this->optimizer->optimize($css_asset);
+ }
+ // Per the W3C specification at
+ // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules must
+ // precede any other style, so we move those to the top. The regular
+ // expression is expressed in NOWDOC since it is detecting backslashes as
+ // well as single and double quotes. It is difficult to read when
+ // represented as a quoted string.
+ $regexp = <<<'REGEXP'
+/@import\s*(?:'(?:\\'|.)*'|"(?:\\"|.)*"|url\(\s*(?:\\[\)\'\"]|[^'")])*\s*\)|url\(\s*'(?:\'|.)*'\s*\)|url\(\s*"(?:\"|.)*"\s*\)).*;/iU
+REGEXP;
+ preg_match_all($regexp, $data, $matches);
+ $data = preg_replace($regexp, '', $data);
+ return implode('', $matches[0]) . $data;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
index 99c8ac6487..ceb8505c43 100644
--- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
@@ -2,11 +2,18 @@
namespace Drupal\Core\Asset;
+@trigger_error('The ' . __NAMESPACE__ . '\JsCollectionOptimizer is deprecated in drupal:10.0.0 and is removed from drupal:11.0.0. Instead, use ' . __NAMESPACE__ . '\JsCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED);
+
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface;
/**
* Optimizes JavaScript assets.
+ *
+ * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead use
+ * \Drupal\Core\Asset\JsCollectionOptimizerLazy.
+ *
+ * @see https://www.drupal.org/node/2888767
*/
class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
@@ -81,7 +88,7 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim
* configurable period (@code system.performance.stale_file_threshold @endcode)
* to ensure that files referenced by a cached page will still be available.
*/
- public function optimize(array $js_assets) {
+ public function optimize(array $js_assets, array $libraries) {
// Group the assets.
$js_groups = $this->grouper->group($js_assets);
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
new file mode 100644
index 0000000000..eee64bb65c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
@@ -0,0 +1,191 @@
+grouper->group($js_assets);
+
+ $js_assets = [];
+ foreach ($js_groups as $order => $js_group) {
+ // We have to return a single asset, not a group of assets. It is now up
+ // to one of the pieces of code in the switch statement below to set the
+ // 'data' property to the appropriate value.
+ $js_assets[$order] = $js_group;
+
+ switch ($js_group['type']) {
+ case 'file':
+ // No preprocessing, single JS asset: just use the existing URI.
+ if (!$js_group['preprocess']) {
+ $uri = $js_group['items'][0]['data'];
+ $js_assets[$order]['data'] = $uri;
+ }
+ else {
+ // To reproduce the full context of assets outside of the request,
+ // we must know the entire set of libraries used to generate all CSS
+ // groups, whether or not files in a group are from a particular
+ // library or not.
+ $js_assets[$order]['preprocessed'] = TRUE;
+ }
+ break;
+
+ case 'external':
+ // We don't do any aggregation and hence also no caching for external
+ // JS assets.
+ $uri = $js_group['items'][0]['data'];
+ $js_assets[$order]['data'] = $uri;
+ break;
+
+ case 'setting':
+ $js_assets[$order]['data'] = $js_group['data'];
+ break;
+ }
+ }
+ if ($libraries) {
+ // Generate a URL for the group, but do not process it inline, this is
+ // done by \Drupal\system\controller\JsAssetController.
+ $ajax_page_state = $this->requestStack->getCurrentRequest()
+ ->get('ajax_page_state');
+ $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [];
+ $language = $this->languageManager->getCurrentLanguage()->getId();
+ $query_args = [
+ 'language' => $language,
+ 'theme' => $this->themeManager->getActiveTheme()->getName(),
+ 'include' => implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($libraries)),
+ ];
+ if ($already_loaded) {
+ $query_args['exclude'] = implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded));
+ }
+ foreach ($js_assets as $order => $js_asset) {
+ if (!empty($js_asset['preprocessed'])) {
+ $query = [
+ 'scope' => $js_asset['scope'] === 'header' ? 'header' : 'footer',
+ 'delta' => "$order",
+ ] + $query_args;
+ $filename = 'js_' . $this->generateHash($js_asset) . '.js';
+ $uri = 'public://js/' . $filename;
+ $js_assets[$order]['data'] = $this->fileUrlGenerator->generateAbsoluteString($uri) . '?' . UrlHelper::buildQuery($query);
+ }
+ unset($js_assets[$order]['items']);
+ }
+ }
+
+ return $js_assets;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAll() {
+ return $this->state->get('system.js_cache_files', []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteAll() {
+ $this->state->delete('system.js_cache_files');
+ $delete_stale = function ($uri) {
+ $threshold = $this->configFactory
+ ->get('system.performance')
+ ->get('stale_file_threshold');
+ // Default stale file threshold is 30 days.
+ if ($this->time->getRequestTime() - filemtime($uri) > $threshold) {
+ $this->fileSystem->delete($uri);
+ }
+ };
+ if (is_dir('public://js')) {
+ $this->fileSystem->scanDirectory('public://js', '/.*/', ['callback' => $delete_stale]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function optimizeGroup(array $group): string {
+ $data = '';
+ foreach ($group['items'] as $js_asset) {
+ // Optimize this JS file, but only if it's not yet minified.
+ if (isset($js_asset['minified']) && $js_asset['minified']) {
+ $data .= file_get_contents($js_asset['data']);
+ }
+ else {
+ $data .= $this->optimizer->optimize($js_asset);
+ }
+ // Append a ';' and a newline after each JS file to prevent them from
+ // running together.
+ $data .= ";\n";
+ }
+ // Remove unwanted JS code that causes issues.
+ return $this->optimizer->clean($data);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
index 7df92bdb6d..95b53422e1 100644
--- a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
+++ b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
@@ -9,6 +9,7 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Component\Utility\Html;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -80,6 +81,13 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
*/
protected $moduleHandler;
+ /**
+ * The language manager.
+ *
+ * @var \Drupal\Core\Language\LanguageManagerInterface
+ */
+ protected $languageManager;
+
/**
* Constructs a HtmlResponseAttachmentsProcessor object.
*
@@ -97,8 +105,10 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The language manager.
*/
- public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
@@ -106,6 +116,11 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
+ if (!isset($language_manager)) {
+ @trigger_error('Calling ' . __METHOD__ . '() without the $language_manager argument is deprecated in drupal:10.1.0 and will be required in drupal:11.0.0', E_USER_DEPRECATED);
+ $language_manager = \Drupal::languageManager();
+ }
+ $this->languageManager = $language_manager;
}
/**
@@ -309,14 +324,14 @@ protected function processAssetLibraries(AttachedAssetsInterface $assets, array
if (isset($placeholders['styles'])) {
// Optimize CSS if necessary, but only during normal site operation.
$optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
- $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css));
+ $variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css, $this->languageManager->getCurrentLanguage()));
}
// Print scripts - if any are present.
if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) {
// Optimize JS if necessary, but only during normal site operation.
$optimize_js = !defined('MAINTENANCE_MODE') && !\Drupal::state()->get('system.maintenance_mode') && $this->config->get('js.preprocess');
- [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, $optimize_js);
+ [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, $optimize_js, $this->languageManager->getCurrentLanguage());
$variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
$variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
}
diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index 3ea1f6c44d..dc329fd6ef 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -824,10 +824,12 @@ function hook_element_plugin_alter(array &$definitions) {
* An array of all JavaScript being presented on the page.
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
+ * @param \Drupal\Core\Language\LanguageInterface $language
+ * The language for the page request that the assets will be rendered for.
*
* @see \Drupal\Core\Asset\AssetResolver
*/
-function hook_js_alter(&$javascript, \Drupal\Core\Asset\AttachedAssetsInterface $assets) {
+function hook_js_alter(&$javascript, \Drupal\Core\Asset\AttachedAssetsInterface $assets, \Drupal\Core\Language\LanguageInterface $language) {
// Swap out jQuery to use an updated version of the library.
$javascript['core/assets/vendor/jquery/jquery.min.js']['data'] = \Drupal::service('extension.list.module')->getPath('jquery_update') . '/jquery.js';
}
@@ -1000,10 +1002,12 @@ function hook_library_info_alter(&$libraries, $extension) {
* An array of all CSS items (files and inline CSS) being requested on the page.
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
+ * @param \Drupal\Core\Language\LanguageInterface $language
+ * The language of the request that the assets will be rendered for.
*
* @see Drupal\Core\Asset\LibraryResolverInterface::getCssAssets()
*/
-function hook_css_alter(&$css, \Drupal\Core\Asset\AttachedAssetsInterface $assets) {
+function hook_css_alter(&$css, \Drupal\Core\Asset\AttachedAssetsInterface $assets, \Drupal\Core\Language\LanguageInterface $language) {
// Remove defaults.css file.
$file_path = \Drupal::service('extension.list.module')->getPath('system') . '/defaults.css';
unset($css[$file_path]);
diff --git a/core/modules/big_pipe/big_pipe.services.yml b/core/modules/big_pipe/big_pipe.services.yml
index ff21df3367..09747e887f 100644
--- a/core/modules/big_pipe/big_pipe.services.yml
+++ b/core/modules/big_pipe/big_pipe.services.yml
@@ -16,7 +16,7 @@ services:
public: false
class: \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
decorates: html_response.attachments_processor
- arguments: ['@html_response.attachments_processor.big_pipe.inner', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
+ arguments: ['@html_response.attachments_processor.big_pipe.inner', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler', '@language_manager']
route_subscriber.no_big_pipe:
class: Drupal\big_pipe\EventSubscriber\NoBigPipeRouteAlterSubscriber
diff --git a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
index 66ed363b6c..fe42a3e564 100644
--- a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
+++ b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php
@@ -7,6 +7,7 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\EnforcedResponseException;
+use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Drupal\Core\Render\HtmlResponse;
@@ -48,10 +49,12 @@ class BigPipeResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcess
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The language manager.
*/
- public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler) {
+ public function __construct(AttachmentsResponseProcessorInterface $html_response_attachments_processor, AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer, ModuleHandlerInterface $module_handler, LanguageManagerInterface $language_manager) {
$this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor;
- parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler);
+ parent::__construct($asset_resolver, $config_factory, $css_collection_renderer, $js_collection_renderer, $request_stack, $renderer, $module_handler, $language_manager);
}
/**
diff --git a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
index 2f77684e55..f91a8bbeb2 100644
--- a/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
+++ b/core/modules/big_pipe/tests/src/Unit/Render/BigPipeResponseAttachmentsProcessorTest.php
@@ -9,6 +9,7 @@
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
use Drupal\Core\Render\HtmlResponse;
@@ -135,7 +136,8 @@ protected function createBigPipeResponseAttachmentsProcessor(ObjectProphecy $dec
$this->prophesize(AssetCollectionRendererInterface::class)->reveal(),
$this->prophesize(RequestStack::class)->reveal(),
$this->prophesize(RendererInterface::class)->reveal(),
- $this->prophesize(ModuleHandlerInterface::class)->reveal()
+ $this->prophesize(ModuleHandlerInterface::class)->reveal(),
+ $this->prophesize(LanguageManagerInterface::class)->reveal()
);
}
diff --git a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
index 0e43f969e4..eca98b75fe 100644
--- a/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/src/Plugin/Editor/CKEditor.php
@@ -355,7 +355,7 @@ public function getJSSettings(Editor $editor) {
// Parse all CKEditor plugin JavaScript files for translations.
if ($this->moduleHandler->moduleExists('locale')) {
- locale_js_translate(array_values($external_plugin_files));
+ locale_js_translate(array_values($external_plugin_files), $language_interface);
}
ksort($settings);
diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module
index 1eb0293a85..19b2a2884f 100644
--- a/core/modules/ckeditor5/ckeditor5.module
+++ b/core/modules/ckeditor5/ckeditor5.module
@@ -18,6 +18,7 @@
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
@@ -375,7 +376,7 @@ function _add_attachments_to_editor_update_response(array $form, AjaxResponse &$
/**
* Returns a list of language codes supported by CKEditor 5.
*
- * @param $lang
+ * @param string|bool $lang
* The Drupal langcode to match.
*
* @return array|mixed|string
@@ -420,7 +421,6 @@ function _ckeditor5_get_langcode_mapping($lang = FALSE) {
unset($langcodes[$langcode]);
}
}
-
if ($lang) {
return $langcodes[$lang] ?? 'en';
}
@@ -544,7 +544,7 @@ function ckeditor5_library_info_alter(&$libraries, $extension) {
/**
* Implements hook_js_alter().
*/
-function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets) {
+function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
// This file means CKEditor 5 translations are in use on the page.
// @see locale_js_alter()
$placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
@@ -566,8 +566,7 @@ function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets) {
return;
}
- $language_interface = \Drupal::languageManager()->getCurrentLanguage()->getId();
- $ckeditor5_language = _ckeditor5_get_langcode_mapping($language_interface);
+ $ckeditor5_language = _ckeditor5_get_langcode_mapping($language->getId());
// Remove all CKEditor 5 translations files that are not in the current
// language.
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 1a30d0bfc1..7fbf1698d6 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -490,7 +490,7 @@ function locale_cache_flush() {
/**
* Implements hook_js_alter().
*/
-function locale_js_alter(&$javascript, AttachedAssetsInterface $assets) {
+function locale_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
// @todo Remove this in https://www.drupal.org/node/2421323.
$files = [];
foreach ($javascript as $item) {
@@ -506,7 +506,7 @@ function locale_js_alter(&$javascript, AttachedAssetsInterface $assets) {
// Replace the placeholder file with the actual JS translation file.
$placeholder_file = 'core/modules/locale/locale.translation.js';
if (isset($javascript[$placeholder_file])) {
- if ($translation_file = locale_js_translate($files)) {
+ if ($translation_file = locale_js_translate($files, $language)) {
$js_translation_asset = &$javascript[$placeholder_file];
$js_translation_asset['data'] = $translation_file;
// @todo Remove this when https://www.drupal.org/node/1945262 lands.
@@ -530,13 +530,17 @@ function locale_js_alter(&$javascript, AttachedAssetsInterface $assets) {
*
* @param array $files
* An array of local file paths.
+ * @param \Drupal\Core\Language\LanguageInterface $language_interface
+ * The interface language the files should be translated into.
*
* @return string|null
* The filepath to the translation file or NULL if no translation is
* applicable.
*/
-function locale_js_translate(array $files = []) {
- $language_interface = \Drupal::languageManager()->getCurrentLanguage();
+function locale_js_translate(array $files = [], $language_interface = NULL) {
+ if (!isset($language_interface)) {
+ $language_interface = \Drupal::languageManager()->getCurrentLanguage();
+ }
$dir = 'public://' . \Drupal::config('locale.settings')->get('javascript.directory');
$parsed = \Drupal::state()->get('system.javascript_parsed', []);
diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module
index e16c213f26..0f3c3a262d 100644
--- a/core/modules/settings_tray/settings_tray.module
+++ b/core/modules/settings_tray/settings_tray.module
@@ -8,6 +8,7 @@
use Drupal\Core\Url;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Language\LanguageInterface;
use Drupal\block\entity\Block;
use Drupal\block\BlockInterface;
use Drupal\settings_tray\Block\BlockEntitySettingTrayForm;
@@ -177,7 +178,7 @@ function settings_tray_block_alter(&$definitions) {
/**
* Implements hook_css_alter().
*/
-function settings_tray_css_alter(&$css, AttachedAssetsInterface $assets) {
+function settings_tray_css_alter(&$css, AttachedAssetsInterface $assets, LanguageInterface $language) {
// @todo Remove once conditional ordering is introduced in
// https://www.drupal.org/node/1945262.
$path = \Drupal::service('extension.list.module')->getPath('settings_tray') . '/css/settings_tray.theme.css';
diff --git a/core/modules/system/src/Controller/AssetControllerBase.php b/core/modules/system/src/Controller/AssetControllerBase.php
new file mode 100644
index 0000000000..6e3a461823
--- /dev/null
+++ b/core/modules/system/src/Controller/AssetControllerBase.php
@@ -0,0 +1,224 @@
+fileExtension = $this->assetType;
+ }
+
+ /**
+ * Generates an aggregate, given a filename.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The request object.
+ * @param string $file_name
+ * The file to deliver.
+ *
+ * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response
+ * The transferred file as response.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * Thrown when the filename is invalid or an invalid query argument is
+ * supplied.
+ */
+ public function deliver(Request $request, string $file_name) {
+ $uri = 'public://' . $this->assetType . '/' . $file_name;
+
+ // Check to see whether a file matching the $uri already exists, this can
+ // happen if it was created while this request was in progress.
+ if (file_exists($uri)) {
+ return new BinaryFileResponse($uri, 200, ['Cache-control' => static::CACHE_CONTROL]);
+ }
+
+ // First validate that the request is valid enough to produce an asset group
+ // aggregate. The theme must be passed as a query parameter, since assets
+ // always depend on the current theme.
+ if (!$request->query->has('theme')) {
+ throw new BadRequestHttpException('The theme must be passed as a query argument');
+ }
+ if (!$request->query->has('delta') || !is_numeric($request->query->get('delta'))) {
+ throw new BadRequestHttpException('The numeric delta must be passed as a query argument');
+ }
+ if (!$request->query->has('language')) {
+ throw new BadRequestHttpException('The language must be passed as a query argument');
+ }
+ $file_parts = explode('_', basename($file_name, '.' . $this->fileExtension));
+
+ // The hash is the second segment of the filename.
+ if (!isset($file_parts[1])) {
+ throw new BadRequestHttpException('Invalid filename');
+ }
+ $received_hash = $file_parts[1];
+
+ // Now build the asset groups based on the libraries. It requires the full
+ // set of asset groups to extract and build the aggregate for the group we
+ // want, since libraries may be split across different asset groups.
+ $theme = $request->query->get('theme');
+ $active_theme = $this->themeInitialization->initTheme($theme);
+ $this->themeManager->setActiveTheme($active_theme);
+
+ $attached_assets = new AttachedAssets();
+ $attached_assets->setLibraries(explode(',', $request->query->get('include')));
+ if ($request->query->has('exclude')) {
+ $attached_assets->setAlreadyLoadedLibraries(explode(',', $request->query->get('exclude')));
+ }
+ $groups = $this->getGroups($attached_assets, $request);
+
+ $group = $this->getGroup($groups, $request->query->get('delta'));
+ // Generate a hash based on the asset group, this uses the same method as
+ // the collection optimizer does to create the filename, so it should match.
+ $generated_hash = $this->generateHash($group);
+ $data = $this->optimizer->optimizeGroup($group);
+
+ // However, the hash from the library definitions in code may not match the
+ // hash from the URL. This can be for three reasons:
+ // 1. Someone has requested an outdated URL, i.e. from a cached page, which
+ // matches a different version of the code base.
+ // 2. Someone has requested an outdated URL during a deployment. This is
+ // the same case as #1 but a much shorter window.
+ // 3. Someone is attempting to craft an invalid URL in order to conduct a
+ // denial of service attack on the site.
+ // Dump the optimized group into an aggregate file, but only if the
+ // received hash and generated hash match. This prevents invalid filenames
+ // from filling the disk, while still serving aggregates that may be
+ // referenced in cached HTML.
+ if ($received_hash === $generated_hash) {
+ $uri = $this->dumper->dumpToUri($data, $this->assetType, $uri);
+ $state_key = 'drupal_' . $this->assetType . '_cache_files';
+ $files = $this->state()->get($state_key, []);
+ $files[] = $uri;
+ $this->state()->set($state_key, $files);
+ }
+ return new Response($data, 200, [
+ 'Cache-control' => static::CACHE_CONTROL,
+ 'Content-Type' => $this->contentType,
+ ]);
+ }
+
+ /**
+ * Gets a group.
+ *
+ * @param array $groups
+ * An array of asset groups.
+ * @param int $group_delta
+ * The group delta.
+ *
+ * @return array
+ * The correct asset group matching $group_delta.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * Thrown when the filename is invalid.
+ */
+ protected function getGroup(array $groups, int $group_delta): array {
+ if (isset($groups[$group_delta])) {
+ return $groups[$group_delta];
+ }
+ throw new BadRequestHttpException('Invalid filename.');
+ }
+
+ /**
+ * Get grouped assets.
+ *
+ * @param \Drupal\Core\Asset\AttachedAssetsInterface $attached_assets
+ * The attached assets.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The current request.
+ *
+ * @return array
+ * The grouped assets.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * Thrown when the query argument is omitted.
+ */
+ abstract protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array;
+
+}
diff --git a/core/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php
new file mode 100644
index 0000000000..76d1e8a8b7
--- /dev/null
+++ b/core/modules/system/src/Controller/CssAssetController.php
@@ -0,0 +1,53 @@
+get('stream_wrapper_manager'),
+ $container->get('library.dependency_resolver'),
+ $container->get('asset.resolver'),
+ $container->get('theme.initialization'),
+ $container->get('theme.manager'),
+ $container->get('asset.css.collection_grouper'),
+ $container->get('asset.css.collection_optimizer'),
+ $container->get('asset.css.dumper'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array {
+ $language = $this->languageManager()->getLanguage($request->get('language'));
+ $assets = $this->assetResolver->getCssAssets($attached_assets, FALSE, $language);
+ return $this->grouper->group($assets);
+ }
+
+}
diff --git a/core/modules/system/src/Controller/JsAssetController.php b/core/modules/system/src/Controller/JsAssetController.php
new file mode 100644
index 0000000000..7900286496
--- /dev/null
+++ b/core/modules/system/src/Controller/JsAssetController.php
@@ -0,0 +1,64 @@
+get('stream_wrapper_manager'),
+ $container->get('library.dependency_resolver'),
+ $container->get('asset.resolver'),
+ $container->get('theme.initialization'),
+ $container->get('theme.manager'),
+ $container->get('asset.js.collection_grouper'),
+ $container->get('asset.js.collection_optimizer'),
+ $container->get('asset.js.dumper'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getGroups(AttachedAssetsInterface $attached_assets, Request $request): array {
+ // The header and footer scripts are two distinct sets of asset groups. The
+ // $group_key is not sufficient to find the group, we also need to locate it
+ // within either the header or footer set.
+ $language = $this->languageManager()->getLanguage($request->get('language'));
+ [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($attached_assets, FALSE, $language);
+ $scope = $request->get('scope');
+ if (!isset($scope)) {
+ throw new BadRequestHttpException('The URL must have a scope query argument.');
+ }
+ $assets = $scope === 'header' ? $js_assets_header : $js_assets_footer;
+ // While the asset resolver will find settings, these are never aggregated,
+ // so filter them out.
+ unset($assets['drupalSettings']);
+ return $this->grouper->group($assets);
+ }
+
+}
diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php
new file mode 100644
index 0000000000..b569824ec5
--- /dev/null
+++ b/core/modules/system/src/Routing/AssetRoutes.php
@@ -0,0 +1,68 @@
+get('stream_wrapper_manager')
+ );
+ }
+
+ /**
+ * Returns an array of route objects.
+ *
+ * @return \Symfony\Component\Routing\Route[]
+ * An array of route objects.
+ */
+ public function routes(): array {
+ $routes = [];
+ // Generate assets. If clean URLs are disabled image derivatives will always
+ // be served through the routing system. If clean URLs are enabled and the
+ // image derivative already exists, PHP will be bypassed.
+ $directory_path = $this->streamWrapperManager->getViaScheme('public')->getDirectoryPath();
+
+ $routes['system.css_asset'] = new Route(
+ '/' . $directory_path . '/css/{file_name}',
+ [
+ '_controller' => 'Drupal\system\Controller\CssAssetController::deliver',
+ ],
+ [
+ '_access' => 'TRUE',
+ ]
+ );
+ $routes['system.js_asset'] = new Route(
+ '/' . $directory_path . '/js/{file_name}',
+ [
+ '_controller' => 'Drupal\system\Controller\JsAssetController::deliver',
+ ],
+ [
+ '_access' => 'TRUE',
+ ]
+ );
+ return $routes;
+ }
+
+}
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 00eddce6b5..362763fa18 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -520,3 +520,6 @@ system.csrftoken:
_controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'
+
+route_callbacks:
+ - '\Drupal\system\Routing\AssetRoutes::routes'
diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module
index b38d5f7bbe..211147ef5b 100644
--- a/core/modules/system/tests/modules/common_test/common_test.module
+++ b/core/modules/system/tests/modules/common_test/common_test.module
@@ -6,6 +6,7 @@
*/
use Drupal\Core\Asset\AttachedAssetsInterface;
+use Drupal\Core\Language\LanguageInterface;
/**
* Applies #printed to an element to help test #pre_render.
@@ -250,7 +251,7 @@ function common_test_page_attachments_alter(array &$page) {
*
* @see \Drupal\KernelTests\Core\Asset\AttachedAssetsTest::testAlter()
*/
-function common_test_js_alter(&$javascript, AttachedAssetsInterface $assets) {
+function common_test_js_alter(&$javascript, AttachedAssetsInterface $assets, LanguageInterface $language) {
// Attach alter.js above tableselect.js.
$alterjs = \Drupal::service('extension.list.module')->getPath('common_test') . '/alter.js';
if (array_key_exists($alterjs, $javascript) && array_key_exists('core/misc/tableselect.js', $javascript)) {
diff --git a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
index de95fb5761..1f4ce798aa 100644
--- a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
+++ b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
@@ -53,12 +53,12 @@ public function testOrder() {
$renderer = \Drupal::service('renderer');
$build['#attached']['library'][] = 'ajax_test/order-css-command';
$assets = AttachedAssets::createFromRenderArray($build);
- $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE));
+ $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()));
$expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array));
$build['#attached']['library'][] = 'ajax_test/order-header-js-command';
$build['#attached']['library'][] = 'ajax_test/order-footer-js-command';
$assets = AttachedAssets::createFromRenderArray($build);
- [$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE);
+ [$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$js_header_render_array = $js_collection_renderer->render($js_assets_header);
$js_footer_render_array = $js_collection_renderer->render($js_assets_footer);
$expected_commands[2] = new PrependCommand('head', $js_header_render_array);
diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon
index 50c770ed62..3fe6bec6b1 100644
--- a/core/phpstan-baseline.neon
+++ b/core/phpstan-baseline.neon
@@ -100,16 +100,6 @@ parameters:
count: 1
path: lib/Drupal/Core/Archiver/ArchiverManager.php
- -
- message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:10\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#"
- count: 1
- path: lib/Drupal/Core/Asset/CssCollectionOptimizer.php
-
- -
- message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:10\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#"
- count: 1
- path: lib/Drupal/Core/Asset/JsCollectionOptimizer.php
-
-
message: "#^Call to deprecated constant REQUEST_TIME\\: Deprecated in drupal\\:8\\.3\\.0 and is removed from drupal\\:10\\.0\\.0\\. Use \\\\Drupal\\:\\:time\\(\\)\\-\\>getRequestTime\\(\\); $#"
count: 1
diff --git a/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php
new file mode 100644
index 0000000000..13aa82390b
--- /dev/null
+++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php
@@ -0,0 +1,203 @@
+config('system.performance')->set('css', [
+ 'preprocess' => TRUE,
+ 'gzip' => TRUE,
+ ])->save();
+ $this->config('system.performance')->set('js', [
+ 'preprocess' => TRUE,
+ 'gzip' => TRUE,
+ ])->save();
+ $user = $this->createUser();
+ $this->drupalLogin($user);
+ $this->drupalGet('');
+ $session = $this->getSession();
+ $page = $session->getPage();
+
+ $elements = $page->findAll('xpath', '//link[@rel="stylesheet"]');
+ $urls = [];
+ foreach ($elements as $element) {
+ if ($element->hasAttribute('href')) {
+ $urls[] = $element->getAttribute('href');
+ }
+ }
+ foreach ($urls as $url) {
+ $this->assertAggregate($url);
+ }
+ foreach ($urls as $url) {
+ $this->assertAggregate($url, FALSE);
+ }
+
+ foreach ($urls as $url) {
+ $this->assertInvalidAggregates($url);
+ }
+
+ $elements = $page->findAll('xpath', '//script');
+ $urls = [];
+ foreach ($elements as $element) {
+ if ($element->hasAttribute('src')) {
+ $urls[] = $element->getAttribute('src');
+ }
+ }
+ foreach ($urls as $url) {
+ $this->assertAggregate($url);
+ }
+ foreach ($urls as $url) {
+ $this->assertAggregate($url, FALSE);
+ }
+ foreach ($urls as $url) {
+ $this->assertInvalidAggregates($url);
+ }
+ }
+
+ /**
+ * Asserts the aggregate header.
+ *
+ * @param string $url
+ * The source URL.
+ * @param bool $from_php
+ * (optional) Is the result from PHP or disk? Defaults to TRUE (PHP).
+ */
+ protected function assertAggregate(string $url, bool $from_php = TRUE): void {
+ $url = $this->getAbsoluteUrl($url);
+ $session = $this->getSession();
+ $session->visit($url);
+ $this->assertSession()->statusCodeEquals(200);
+ $headers = $session->getResponseHeaders();
+ if ($from_php) {
+ $this->assertEquals(['no-store, private'], $headers['Cache-Control']);
+ }
+ else {
+ $this->assertArrayNotHasKey('Cache-Control', $headers);
+ }
+ }
+
+ /**
+ * Asserts the aggregate when it is invalid.
+ *
+ * @param string $url
+ * The source URL.
+ *
+ * @throws \Behat\Mink\Exception\ExpectationException
+ */
+ protected function assertInvalidAggregates(string $url): void {
+ $session = $this->getSession();
+ $session->visit($this->replaceGroupDelta($url));
+ $this->assertSession()->statusCodeEquals(200);
+
+ $session->visit($this->omitTheme($url));
+ $this->assertSession()->statusCodeEquals(400);
+
+ $session->visit($this->setInvalidLibrary($url));
+ $this->assertSession()->statusCodeEquals(200);
+
+ $session->visit($this->replaceGroupHash($url));
+ $this->assertSession()->statusCodeEquals(200);
+ $headers = $session->getResponseHeaders();
+ $this->assertEquals(['no-store, private'], $headers['Cache-Control']);
+
+ // And again to confirm it's not cached on disk.
+ $session->visit($this->replaceGroupHash($url));
+ $this->assertSession()->statusCodeEquals(200);
+ $headers = $session->getResponseHeaders();
+ $this->assertEquals(['no-store, private'], $headers['Cache-Control']);
+ }
+
+ /**
+ * Replaces the delta in the given URL.
+ *
+ * @param string $url
+ * The source URL.
+ *
+ * @return string
+ * The URL with the delta replaced.
+ */
+ protected function replaceGroupDelta(string $url): string {
+ $parts = UrlHelper::parse($url);
+ $parts['query']['delta'] = 100;
+ $query = UrlHelper::buildQuery($parts['query']);
+ return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
+ }
+
+ /**
+ * Replaces the group hash in the given URL.
+ *
+ * @param string $url
+ * The source URL.
+ *
+ * @return string
+ * The URL with the group hash replaced.
+ */
+ protected function replaceGroupHash(string $url): string {
+ $parts = explode('_', $url);
+ $hash = strtok($parts[1], '.');
+ $parts[1] = str_replace($hash, 'abcdefghijklmnop', $parts[1]);
+ return $this->getAbsoluteUrl(implode('_', $parts));
+ }
+
+ /**
+ * Replaces the 'libraries' entry in the given URL with an invalid value.
+ *
+ * @param string $url
+ * The source URL.
+ *
+ * @return string
+ * The URL with the 'library' query set to an invalid value.
+ */
+ protected function setInvalidLibrary(string $url): string {
+ // First replace the hash, so we don't get served the actual file on disk.
+ $url = $this->replaceGroupHash($url);
+ $parts = UrlHelper::parse($url);
+ $parts['query']['libraries'] = ['system/llama'];
+
+ $query = UrlHelper::buildQuery($parts['query']);
+ return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
+ }
+
+ /**
+ * Removes the 'theme' query parameter from the given URL.
+ *
+ * @param string $url
+ * The source URL.
+ *
+ * @return string
+ * The URL with the 'theme' omitted.
+ */
+ protected function omitTheme(string $url): string {
+ // First replace the hash, so we don't get served the actual file on disk.
+ $url = $this->replaceGroupHash($url);
+ $parts = UrlHelper::parse($url);
+ unset($parts['query']['theme']);
+ $query = UrlHelper::buildQuery($parts['query']);
+ return $this->getAbsoluteUrl($parts['path'] . '?' . $query . '#' . $parts['fragment']);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
index 0a98220c14..26185c6a2c 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
@@ -63,8 +63,8 @@ protected function setUp(): void {
*/
public function testDefault() {
$assets = new AttachedAssets();
- $this->assertEquals([], $this->assetResolver->getCssAssets($assets, FALSE), 'Default CSS is empty.');
- [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE);
+ $this->assertEquals([], $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()), 'Default CSS is empty.');
+ [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$this->assertEquals([], $js_assets_header, 'Default header JavaScript is empty.');
$this->assertEquals([], $js_assets_footer, 'Default footer JavaScript is empty.');
}
@@ -76,7 +76,7 @@ public function testLibraryUnknown() {
$build['#attached']['library'][] = 'core/unknown';
$assets = AttachedAssets::createFromRenderArray($build);
- $this->assertSame([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.');
+ $this->assertSame([], $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0], 'Unknown library was not added to the page.');
}
/**
@@ -86,8 +86,8 @@ public function testAddFiles() {
$build['#attached']['library'][] = 'common_test/files';
$assets = AttachedAssets::createFromRenderArray($build);
- $css = $this->assetResolver->getCssAssets($assets, FALSE);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/bar.css', $css);
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/foo.js', $js);
@@ -109,12 +109,12 @@ public function testAddJsSettings() {
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertEquals([], $assets->getSettings(), 'JavaScript settings on $assets are empty.');
- $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('currentPath', $javascript['drupalSettings']['data']['path']);
$this->assertArrayHasKey('currentPath', $assets->getSettings()['path']);
$assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]);
- $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertEquals(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.');
$this->assertEquals('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.');
}
@@ -126,8 +126,8 @@ public function testAddExternalFiles() {
$build['#attached']['library'][] = 'common_test/external';
$assets = AttachedAssets::createFromRenderArray($build);
- $css = $this->assetResolver->getCssAssets($assets, FALSE);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('http://example.com/stylesheet.css', $css);
$this->assertArrayHasKey('http://example.com/script.js', $js);
@@ -146,7 +146,7 @@ public function testAttributes() {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$expected_1 = '';
@@ -162,7 +162,7 @@ public function testAggregatedAttributes() {
$build['#attached']['library'][] = 'common_test/js-attributes';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, TRUE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$expected_1 = '';
@@ -179,9 +179,9 @@ public function testAggregation() {
$build['#attached']['library'][] = 'core/drupal.vertical-tabs';
$assets = AttachedAssets::createFromRenderArray($build);
- $this->assertCount(1, $this->assetResolver->getCssAssets($assets, TRUE), 'There is a sole aggregated CSS asset.');
+ $this->assertCount(1, $this->assetResolver->getCssAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage()), 'There is a sole aggregated CSS asset.');
- [$header_js, $footer_js] = $this->assetResolver->getJsAssets($assets, TRUE);
+ [$header_js, $footer_js] = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage());
$this->assertEquals([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.');
$rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js);
$this->assertCount(2, $rendered_footer_js, 'There are 2 JavaScript assets in the footer.');
@@ -199,7 +199,7 @@ public function testSettings() {
$build['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
// Cast to string since this returns a \Drupal\Core\Render\Markup object.
$rendered_js = (string) $this->renderer->renderPlain($js_render_array);
@@ -236,7 +236,7 @@ public function testHeaderHTML() {
$build['#attached']['library'][] = 'common_test/js-header';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[0];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0';
@@ -252,7 +252,7 @@ public function testNoCache() {
$build['#attached']['library'][] = 'common_test/no-cache';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertFalse($js['core/modules/system/tests/modules/common_test/nocache.js']['preprocess'], 'Setting cache to FALSE sets preprocess to FALSE when adding JavaScript.');
}
@@ -263,7 +263,7 @@ public function testVersionQueryString() {
$build['#attached']['library'][] = 'core/once';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
@@ -293,7 +293,7 @@ public function testRenderOrder() {
];
// Retrieve the rendered JavaScript and test against the regex.
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$matches = [];
@@ -335,7 +335,7 @@ public function testRenderOrder() {
];
// Retrieve the rendered CSS and test against the regex.
- $css = $this->assetResolver->getCssAssets($assets, FALSE);
+ $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
$css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css);
$rendered_css = $this->renderer->renderPlain($css_render_array);
$matches = [];
@@ -358,7 +358,7 @@ public function testRenderDifferentWeight() {
$build['#attached']['library'][] = 'common_test/weight';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
// Verify that lighter CSS assets are rendered first.
@@ -383,7 +383,7 @@ public function testAlter() {
// Render the JavaScript, testing if alter.js was altered to be before
// tableselect.js. See common_test_js_alter() to see where this alteration
// takes place.
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
// Verify that JavaScript weight is correctly altered by the alter hook.
@@ -405,7 +405,7 @@ public function testLibraryAlter() {
// common_test_library_info_alter() also added a dependency on jQuery Form.
$build['#attached']['library'][] = 'core/jquery.farbtastic';
$assets = AttachedAssets::createFromRenderArray($build);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js);
$rendered_js = $this->renderer->renderPlain($js_render_array);
$this->assertStringContainsString('core/assets/vendor/jquery-form/jquery.form.min.js', (string) $rendered_js, 'Altered library dependencies are added to the page.');
@@ -450,8 +450,8 @@ public function testAddJsFileWithQueryString() {
$build['#attached']['library'][] = 'common_test/querystring';
$assets = AttachedAssets::createFromRenderArray($build);
- $css = $this->assetResolver->getCssAssets($assets, FALSE);
- $js = $this->assetResolver->getJsAssets($assets, FALSE)[1];
+ $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage());
+ $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1];
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css);
$this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js);
diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php
index f9f58764db..cc3f12bc5b 100644
--- a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php
@@ -68,6 +68,16 @@ class AssetResolverTest extends UnitTestCase {
*/
protected $cache;
+ /**
+ * A mocked English language object.
+ */
+ protected $english;
+
+ /**
+ * A mocked Japanese language object.
+ */
+ protected $japanese;
+
/**
* {@inheritdoc}
*/
@@ -95,10 +105,12 @@ protected function setUp(): void {
$english->expects($this->any())
->method('getId')
->willReturn('en');
+ $this->english = $english;
$japanese = $this->createMock('\Drupal\Core\Language\LanguageInterface');
$japanese->expects($this->any())
->method('getId')
->willReturn('jp');
+ $this->japanese = $japanese;
$this->languageManager = $this->createMock('\Drupal\Core\Language\LanguageManagerInterface');
$this->languageManager->expects($this->any())
->method('getCurrentLanguage')
@@ -113,8 +125,8 @@ protected function setUp(): void {
* @dataProvider providerAttachedAssets
*/
public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
- $this->assetResolver->getCssAssets($assets_a, FALSE);
- $this->assetResolver->getCssAssets($assets_b, FALSE);
+ $this->assetResolver->getCssAssets($assets_a, FALSE, $this->english);
+ $this->assetResolver->getCssAssets($assets_b, FALSE, $this->english);
$this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
}
@@ -123,12 +135,12 @@ public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAsse
* @dataProvider providerAttachedAssets
*/
public function testGetJsAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) {
- $this->assetResolver->getJsAssets($assets_a, FALSE);
- $this->assetResolver->getJsAssets($assets_b, FALSE);
+ $this->assetResolver->getJsAssets($assets_a, FALSE, $this->english);
+ $this->assetResolver->getJsAssets($assets_b, FALSE, $this->english);
$this->assertCount($expected_cache_item_count, $this->cache->getAllCids());
- $this->assetResolver->getJsAssets($assets_a, FALSE);
- $this->assetResolver->getJsAssets($assets_b, FALSE);
+ $this->assetResolver->getJsAssets($assets_a, FALSE, $this->japanese);
+ $this->assetResolver->getJsAssets($assets_b, FALSE, $this->japanese);
$this->assertCount($expected_cache_item_count * 2, $this->cache->getAllCids());
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
new file mode 100644
index 0000000000..9371d9dc9c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
@@ -0,0 +1,89 @@
+createMock(AssetCollectionGrouperInterface::class);
+ $mock_grouper->method('group')
+ ->willReturnCallback(function ($assets) {
+ return [
+ [
+ 'items' => $assets,
+ 'type' => 'file',
+ 'preprocess' => TRUE,
+ ],
+ ];
+ });
+ $mock_optimizer = $this->createMock(AssetOptimizerInterface::class);
+ $mock_optimizer->method('optimize')
+ ->willReturn(
+ file_get_contents(__DIR__ . '/css_test_files/css_input_with_import.css.optimized.css'),
+ file_get_contents(__DIR__ . '/css_test_files/css_subfolder/css_input_with_import.css.optimized.css')
+ );
+ $mock_theme_manager = $this->createMock(ThemeManagerInterface::class);
+ $mock_dependency_resolver = $this->createMock(LibraryDependencyResolverInterface::class);
+ $mock_state = $this->createMock(StateInterface::class);
+ $mock_file_system = $this->createMock(FileSystemInterface::class);
+ $mock_config_factory = $this->createMock(ConfigFactoryInterface::class);
+ $mock_file_url_generator = $this->createMock(FileUrlGeneratorInterface::class);
+ $mock_time = $this->createMock(TimeInterface::class);
+ $mock_language = $this->createMock(LanguageManagerInterface::class);
+ $this->optimizer = new CssCollectionOptimizerLazy($mock_grouper, $mock_optimizer, $mock_theme_manager, $mock_dependency_resolver, new RequestStack(), $mock_state, $mock_file_system, $mock_config_factory, $mock_file_url_generator, $mock_time, $mock_language);
+ $aggregate = $this->optimizer->optimizeGroup(
+ [
+ 'items' => [
+ 'core/modules/system/tests/modules/common_test/common_test_css_import.css' => [
+ 'type' => 'file',
+ 'data' => 'core/modules/system/tests/modules/common_test/common_test_css_import.css',
+ 'preprocess' => TRUE,
+ ],
+ 'core/modules/system/tests/modules/common_test/common_test_css_import_not_preprocessed.css' => [
+ 'type' => 'file',
+ 'data' => 'core/modules/system/tests/modules/common_test/common_test_css_import.css',
+ 'preprocess' => TRUE,
+ ],
+ ],
+ ],
+ );
+ self::assertStringEqualsFile(__DIR__ . '/css_test_files/css_input_with_import.css.optimized.aggregated.css', $aggregate);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php
index a27ce06197..f08dd9e1e3 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php
@@ -2,6 +2,7 @@
namespace Drupal\Tests\Core\Asset;
+use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
use Drupal\Core\Asset\AssetDumperInterface;
use Drupal\Core\Asset\AssetOptimizerInterface;
@@ -31,8 +32,12 @@ class CssCollectionOptimizerUnitTest extends UnitTestCase {
*/
protected $optimizer;
- protected function setUp(): void {
- parent::setUp();
+ /**
+ * Tests that CSS imports with strange letters do not destroy the CSS output.
+ *
+ * @group legacy
+ */
+ public function testCssImport() {
$mock_grouper = $this->createMock(AssetCollectionGrouperInterface::class);
$mock_grouper->method('group')
->willReturnCallback(function ($assets) {
@@ -57,13 +62,8 @@ protected function setUp(): void {
});
$mock_state = $this->createMock(StateInterface::class);
$mock_file_system = $this->createMock(FileSystemInterface::class);
- $this->optimizer = new CssCollectionOptimizer($mock_grouper, $mock_optimizer, $mock_dumper, $mock_state, $mock_file_system);
- }
-
- /**
- * Test that css imports with strange letters do not destroy the css output.
- */
- public function testCssImport() {
+ $mock_time = $this->createMock(TimeInterface::class);
+ $this->optimizer = new CssCollectionOptimizer($mock_grouper, $mock_optimizer, $mock_dumper, $mock_state, $mock_file_system, $mock_time);
$this->optimizer->optimize([
'core/modules/system/tests/modules/common_test/common_test_css_import.css' => [
'type' => 'file',
@@ -75,7 +75,8 @@ public function testCssImport() {
'data' => 'core/modules/system/tests/modules/common_test/common_test_css_import.css',
'preprocess' => TRUE,
],
- ]);
+ ],
+ []);
self::assertEquals(file_get_contents(__DIR__ . '/css_test_files/css_input_with_import.css.optimized.aggregated.css'), $this->dumperData);
}