diff --git a/core/core.services.yml b/core/core.services.yml
index 12440da791..1e0bffbb6d 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..46dcd6aea4 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) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
@@ -96,6 +106,7 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
+ $this->languageManager = $language_manager;
}
/**
@@ -141,8 +152,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..846770ba0c
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php
@@ -0,0 +1,21 @@
+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..db35ba1faa
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php
@@ -0,0 +1,25 @@
+ $asset) {
+ $normalized['asset_group']['items'][$key] = array_diff_key($asset, $group_keys, $omit_keys);
+ }
+ return hash('sha256', serialize($normalized));
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php
index fd35cbc448..bd1fefa236 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,7 +110,7 @@ protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
/**
* {@inheritdoc}
*/
- public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
+ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_library_info_alter().
@@ -143,6 +144,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($css) / 1000;
+ $options['_library'] = $library;
// CSS files are being keyed by the full path.
$css[$options['data']] = $options;
@@ -151,14 +153,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::sort');
if ($optimize) {
- $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
+ $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css, $libraries_to_load);
}
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
@@ -194,7 +196,7 @@ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
/**
* {@inheritdoc}
*/
- public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
+ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, $language) {
$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
@@ -250,6 +252,10 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
// order.
$options['weight'] += count($javascript) / 1000;
+ // Add the library so that it can be identified from the individual
+ // asset.
+ $options['_library'] = $library;
+
// Local and external files must keep their name as the associative
// key so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options;
@@ -258,8 +264,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::sort');
@@ -278,8 +284,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..02a36a8418 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
+ * The language for the request that 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);
/**
* 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
+ * The language for the request that 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);
}
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
index 7d47dad86e..35613970b0 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
@@ -2,11 +2,19 @@
namespace Drupal\Core\Asset;
+@trigger_error('The ' . __NAMESPACE__ . '\CssCollectionOptimizer is deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Instead, use ' . __NAMESPACE__ . '\CssCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED);
+
+use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface;
/**
* Optimizes CSS assets.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Instead, use
+ * \Drupal\Core\Asset\CssCollectionOptimizerLazy.
+ *
+ * @see https://www.drupal.org/node/2888767
*/
class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
@@ -45,6 +53,13 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
*/
protected $fileSystem;
+ /**
+ * The time service.
+ *
+ * @var \Drupal\Component\Datetime\TimeInterface
+ */
+ protected $time;
+
/**
* Constructs a CssCollectionOptimizer.
*
@@ -58,13 +73,23 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
* The state key/value store.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
+ * @param \Drupal\Component\Datetime\TimeInterface $time
+ * The time service.
*/
- public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, FileSystemInterface $file_system) {
+ public function __construct(
+ AssetCollectionGrouperInterface $grouper,
+ AssetOptimizerInterface $optimizer,
+ AssetDumperInterface $dumper,
+ StateInterface $state,
+ FileSystemInterface $file_system,
+ TimeInterface $time
+ ) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
$this->fileSystem = $file_system;
+ $this->time = $time;
}
/**
@@ -81,7 +106,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);
@@ -115,23 +140,7 @@ public function optimize(array $css_assets) {
$uri = $map[$key];
}
if (empty($uri) || !file_exists($uri)) {
- // Optimize each asset within the group.
- $data = '';
- foreach ($css_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);
- $data = implode('', $matches[0]) . $data;
+ $data = $this->optimizeGroup($css_group);
// Dump the optimized CSS for this group into an aggregate file.
$uri = $this->dumper->dump($data, 'css');
// Set the URI for this group's aggregate file.
@@ -180,8 +189,24 @@ protected function generateHash(array $css_group) {
/**
* {@inheritdoc}
*/
- public function getAll() {
- return $this->state->get('drupal_css_cache_files');
+ 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;
}
/**
@@ -192,7 +217,8 @@ public function deleteAll() {
$delete_stale = function ($uri) {
// Default stale file threshold is 30 days.
- if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
+ if ($this->time->getRequestTime() - filemtime($uri)
+ > \Drupal::config('system.performance')->get('stale_file_threshold')) {
$this->fileSystem->delete($uri);
}
};
@@ -201,4 +227,11 @@ public function deleteAll() {
}
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getAll() {
+ return $this->state->get('drupal_css_cache_files', []);
+ }
+
}
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
new file mode 100644
index 0000000000..46947b2ba8
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php
@@ -0,0 +1,273 @@
+grouper = $grouper;
+ $this->optimizer = $optimizer;
+ $this->themeManager = $theme_manager;
+ $this->dependencyResolver = $dependency_resolver;
+ $this->requestStack = $request_stack;
+ $this->state = $state;
+ $this->fileSystem = $file_system;
+ $this->configFactory = $config_factory;
+ $this->fileUrlGenerator = $file_url_generator;
+ $this->time = $time;
+ $this->languageManager = $language_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * File names are generated based on library/asset definitions. This includes
+ * a hash of the assets and the group index. Additionally, the full set of
+ * libraries, already loaded libraries and theme are sent as query parameters
+ * to allow a PHP controller to generate a valid file with sufficient
+ * information. Files are not generated by this method since they're assumed
+ * to be successfully returned from the URL created whether on disk or not.
+ */
+ public function optimize(array $css_assets, array $libraries) {
+ // Group the assets.
+ $css_groups = $this->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;
+
+ switch ($css_group['type']) {
+ case '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;
+ }
+ break;
+
+ case '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;
+ $libraries[$css_group['items'][0]['_library']] = $css_group['items'][0]['_library'];
+ break;
+ }
+ }
+ // 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']) : [];
+ $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 ($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..4406a73094 100644
--- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
@@ -2,11 +2,19 @@
namespace Drupal\Core\Asset;
+@trigger_error('The ' . __NAMESPACE__ . '\JsCollectionOptimizer is deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Instead, use ' . __NAMESPACE__ . '\JsCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED);
+
+use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\State\StateInterface;
/**
* Optimizes JavaScript assets.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:10.0.0. Instead use
+ * use \Drupal\Core\Asset\JsCollectionOptimizerLazy.
+ *
+ * @see https://www.drupal.org/node/2888767
*/
class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
@@ -45,6 +53,13 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
*/
protected $fileSystem;
+ /**
+ * The time service.
+ *
+ * @var \Drupal\Component\Datetime\TimeInterface
+ */
+ protected $time;
+
/**
* Constructs a JsCollectionOptimizer.
*
@@ -58,13 +73,23 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
* The state key/value store.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
+ * @param \Drupal\Component\Datetime\TimeInterface $time
+ * The time service.
*/
- public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, FileSystemInterface $file_system) {
+ public function __construct(
+ AssetCollectionGrouperInterface $grouper,
+ AssetOptimizerInterface $optimizer,
+ AssetDumperInterface $dumper,
+ StateInterface $state,
+ FileSystemInterface $file_system,
+ TimeInterface $time
+ ) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
$this->fileSystem = $file_system;
+ $this->time = $time;
}
/**
@@ -81,7 +106,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);
@@ -115,22 +140,7 @@ public function optimize(array $js_assets) {
$uri = $map[$key];
}
if (empty($uri) || !file_exists($uri)) {
- // Concatenate each asset within the group.
- $data = '';
- foreach ($js_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 cause issues.
- $data = $this->optimizer->clean($data);
+ $data = $this->optimizeGroup($js_group);
// Dump the optimized JS for this group into an aggregate file.
$uri = $this->dumper->dump($data, 'js');
// Set the URI for this group's aggregate file.
@@ -160,7 +170,7 @@ public function optimize(array $js_assets) {
}
/**
- * Generate a hash for a given group of JavaScript assets.
+ * Generates a hash for a given group of JavaScript assets.
*
* @param array $js_group
* A group of JavaScript assets.
@@ -190,7 +200,8 @@ public function deleteAll() {
$this->state->delete('system.js_cache_files');
$delete_stale = function ($uri) {
// Default stale file threshold is 30 days.
- if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
+ if ($this->time->getRequestTime() - filemtime($uri)
+ > \Drupal::config('system.performance')->get('stale_file_threshold')) {
$this->fileSystem->delete($uri);
}
};
@@ -199,4 +210,25 @@ public function deleteAll() {
}
}
+ /**
+ * {@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 cause issues.
+ return $this->optimizer->clean($data);
+ }
+
}
diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
new file mode 100644
index 0000000000..ecee737384
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php
@@ -0,0 +1,279 @@
+grouper = $grouper;
+ $this->optimizer = $optimizer;
+ $this->themeManager = $theme_manager;
+ $this->dependencyResolver = $dependency_resolver;
+ $this->requestStack = $request_stack;
+ $this->state = $state;
+ $this->fileSystem = $file_system;
+ $this->configFactory = $config_factory;
+ $this->fileUrlGenerator = $file_url_generator;
+ $this->time = $time;
+ $this->languageManager = $language_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * File names are generated based on library/asset definitions. This includes
+ * a hash of the assets and the group index. Additionally, the full set of
+ * libraries, already loaded libraries and theme are sent as query parameters
+ * to allow a PHP controller to generate a valid file with sufficient
+ * information. Files are not generated by this method since they're assumed
+ * to be successfully returned from the URL created whether on disk or not.
+ */
+ public function optimize(array $js_assets, array $libraries) {
+ // Group the assets.
+ $js_groups = $this->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..5eb1b2ba31 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,7 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
+ $this->languageManager = $language_manager;
}
/**
@@ -309,14 +320,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..e7b94bc880
--- /dev/null
+++ b/core/modules/system/src/Controller/AssetControllerBase.php
@@ -0,0 +1,314 @@
+libraryDependencyResolver = $library_dependency_resolver;
+ $this->assetResolver = $asset_resolver;
+ $this->themeInitialization = $theme_initialization;
+ $this->themeManager = $theme_manager;
+ $this->grouper = $grouper;
+ $this->optimizer = $optimizer;
+ $this->state = $state;
+ $this->dumper = $dumper;
+ $this->languageManager = $language_manager;
+ $this->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' => $this->cacheControl]);
+ }
+
+ // 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);
+ // @todo Set the language.
+ // $language = $request->query->get('language');
+
+ $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);
+ }
+ // Headers sent from PHP can never perfectly match those sent when the
+ // file is served by the filesystem, so ensure this request does not get
+ // cached in either the browser or reverse proxies. Subsequent requests
+ // for the file will be served from disk and be cached. This is done to
+ // avoid situations such as where one CDN endpoint is serving a version
+ // cached from PHP, while another is serving a version cached from disk.
+ // Should there be any discrepancy in behaviour between those files, this
+ // can make debugging very difficult.
+ return new Response($data, 200, [
+ 'Cache-control' => $this->cacheControl,
+ '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..93e4ec7b67
--- /dev/null
+++ b/core/modules/system/src/Controller/CssAssetController.php
@@ -0,0 +1,55 @@
+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'),
+ $container->get('state'),
+ $container->get('language_manager')
+ );
+ }
+
+ /**
+ * {@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..bbc69fe556
--- /dev/null
+++ b/core/modules/system/src/Controller/JsAssetController.php
@@ -0,0 +1,66 @@
+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'),
+ $container->get('state'),
+ $container->get('language_manager')
+ );
+ }
+
+ /**
+ * {@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..09f0caebef
--- /dev/null
+++ b/core/modules/system/src/Routing/AssetRoutes.php
@@ -0,0 +1,75 @@
+streamWrapperManager = $stream_wrapper_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->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..f5582f231f 100644
--- a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
+++ b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php
@@ -53,7 +53,7 @@ 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';
diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon
index 797a382f0e..61e0f59f3a 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..4f31e28bac 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()->getCurrent()), 'Default CSS is empty.');
+ [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrent());
$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()->getLanguage())[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/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..26eb03d0ab 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',