Issue #1014086 by catch, nod_, martin107, quietone, dww, mariacha1, Spokje, yogeshmpawar, pounard, Wim Leers, mfer, mikeytown2, mbutcher, moshe weitzman, Fabianx, borisson_, alexpott, donquixote, sun, andypost, dawehner: Stampedes and cold cache performance issues with css/js aggregation

merge-requests/2660/head
Alex Pott 2022-08-08 10:12:23 +01:00
parent 047faa3e96
commit b957382d75
No known key found for this signature in database
GPG Key ID: BDA67E7EE836E5CE
35 changed files with 1324 additions and 101 deletions

View File

@ -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', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager', '@state']
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', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager', '@state']
asset.js.optimizer:
class: Drupal\Core\Asset\JsOptimizer
asset.js.collection_grouper:

View File

@ -7,6 +7,7 @@ use Drupal\Core\Asset\AssetResolverInterface;
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;
@ -87,8 +88,10 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* 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, protected LanguageManagerInterface $languageManager) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
@ -96,6 +99,10 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
if (!isset($languageManager)) {
@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);
$this->languageManager = \Drupal::languageManager();
}
}
/**
@ -141,8 +148,8 @@ class AjaxResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
$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,

View File

@ -0,0 +1,23 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that optimizes a collection of assets.
*
* Contains an additional method to allow for optimizing an asset group.
*/
interface AssetCollectionGroupOptimizerInterface extends AssetCollectionOptimizerInterface {
/**
* Optimizes a specific group of assets.
*
* @param array $group
* An asset group.
*
* @return string
* The optimized string for the group.
*/
public function optimizeGroup(array $group): string;
}

View File

@ -12,11 +12,13 @@ interface AssetCollectionOptimizerInterface {
*
* @param array $assets
* An asset collection.
* @param array $libraries
* An array of library names.
*
* @return array
* An optimized asset collection.
*/
public function optimize(array $assets);
public function optimize(array $assets, array $libraries);
/**
* Returns all optimized asset collections assets.

View File

@ -9,7 +9,7 @@ use Drupal\Core\File\FileSystemInterface;
/**
* Dumps a CSS or JavaScript asset.
*/
class AssetDumper implements AssetDumperInterface {
class AssetDumper implements AssetDumperUriInterface {
/**
* The file system service.
@ -36,12 +36,19 @@ class AssetDumper implements AssetDumperInterface {
* browsers to download new CSS when the CSS changes.
*/
public function dump($data, $file_extension) {
$path = 'public://' . $file_extension;
// Prefix filename to prevent blocking by firewalls which reject files
// starting with "ad*".
$filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension;
// Create the css/ or js/ path within the files folder.
$path = 'public://' . $file_extension;
$uri = $path . '/' . $filename;
return $this->dumpToUri($data, $file_extension, $uri);
}
/**
* {@inheritdoc}
*/
public function dumpToUri(string $data, string $file_extension, string $uri): string {
$path = 'public://' . $file_extension;
// Create the CSS or JS file.
$this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);
try {

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\Core\Asset;
/**
* Interface defining a service that dumps an asset to a specified location.
*/
interface AssetDumperUriInterface extends AssetDumperInterface {
/**
* Dumps an (optimized) asset to persistent storage.
*
* @param string $data
* The asset's contents.
* @param string $file_extension
* The file extension of this asset.
* @param string $uri
* The URI to dump to.
*
* @return string
* An URI to access the dumped asset.
*/
public function dumpToUri(string $data, string $file_extension, string $uri): string;
}

View File

@ -0,0 +1,46 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
/**
* Provides a method to generate a normalized hash of a given asset group set.
*/
trait AssetGroupSetHashTrait {
/**
* Generates a hash for an array of asset groups.
*
* @param array $group
* An asset group.
*
* @return string
* A hash to uniquely identify the groups.
*/
protected function generateHash(array $group): string {
$normalized = [];
$group_keys = [
'type' => 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::hmacBase64(serialize($normalized), Settings::getHashSalt());
}
}

View File

@ -7,6 +7,7 @@ use Drupal\Component\Utility\NestedArray;
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 @@ class AssetResolver implements AssetResolverInterface {
/**
* {@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 @@ class AssetResolver implements AssetResolverInterface {
}
// 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 @@ class AssetResolver implements AssetResolverInterface {
/**
* {@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 @@ class AssetResolver implements AssetResolverInterface {
}
// 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 @@ class AssetResolver implements AssetResolverInterface {
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

View File

@ -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 @@ interface AssetResolverInterface {
* @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 @@ interface AssetResolverInterface {
* - 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);
}

View File

@ -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 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
* 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);

View File

@ -0,0 +1,180 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Optimizes CSS assets.
*/
class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface {
use AssetGroupSetHashTrait;
/**
* Constructs a CssCollectionOptimizerLazy.
*
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The grouper for CSS assets.
* @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
* The asset optimizer.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $dependencyResolver
* The library dependency resolver.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
* The file URL generator.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(
protected readonly AssetCollectionGrouperInterface $grouper,
protected readonly AssetOptimizerInterface $optimizer,
protected readonly ThemeManagerInterface $themeManager,
protected readonly LibraryDependencyResolverInterface $dependencyResolver,
protected readonly RequestStack $requestStack,
protected readonly FileSystemInterface $fileSystem,
protected readonly ConfigFactoryInterface $configFactory,
protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
protected readonly TimeInterface $time,
protected readonly LanguageManagerInterface $languageManager,
protected readonly StateInterface $state
) {}
/**
* {@inheritdoc}
*/
public function optimize(array $css_assets, array $libraries) {
// 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.
// 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;
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;
}
}

View File

@ -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 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
* 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);

View File

@ -0,0 +1,191 @@
<?php
namespace Drupal\Core\Asset;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Optimizes JavaScript assets.
*/
class JsCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface {
use AssetGroupSetHashTrait;
/**
* Constructs a JsCollectionOptimizerLazy.
*
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The grouper for JS assets.
* @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
* The asset optimizer.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $dependencyResolver
* The library dependency resolver.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config factory.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
* The file URL generator.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\State\StateInterface $state
* The state key/value store.
*/
public function __construct(
protected readonly AssetCollectionGrouperInterface $grouper,
protected readonly AssetOptimizerInterface $optimizer,
protected readonly ThemeManagerInterface $themeManager,
protected readonly LibraryDependencyResolverInterface $dependencyResolver,
protected readonly RequestStack $requestStack,
protected readonly FileSystemInterface $fileSystem,
protected readonly ConfigFactoryInterface $configFactory,
protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
protected readonly TimeInterface $time,
protected readonly LanguageManagerInterface $languageManager,
protected readonly StateInterface $state
) {}
/**
* {@inheritdoc}
*/
public function optimize(array $js_assets, array $libraries) {
// 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.
// 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);
}
}

View File

@ -9,6 +9,7 @@ use Drupal\Core\Asset\AttachedAssetsInterface;
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;
@ -97,8 +98,10 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
* The renderer.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* 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, protected LanguageManagerInterface $languageManager) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
@ -106,6 +109,10 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
$this->requestStack = $request_stack;
$this->renderer = $renderer;
$this->moduleHandler = $module_handler;
if (!isset($languageManager)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $languageManager argument is deprecated in drupal:10.1.0 and will be required in drupal:11.0.0', E_USER_DEPRECATED);
$this->languageManager = \Drupal::languageManager();
}
}
/**
@ -309,14 +316,14 @@ class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorIn
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);
}

View File

@ -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]);

View File

@ -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

View File

@ -7,6 +7,7 @@ use Drupal\Core\Asset\AssetResolverInterface;
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);
}
/**

View File

@ -9,6 +9,7 @@ use Drupal\Core\Asset\AssetCollectionRendererInterface;
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 @@ class BigPipeResponseAttachmentsProcessorTest extends UnitTestCase {
$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()
);
}

View File

@ -355,7 +355,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
// 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);

View File

@ -18,6 +18,7 @@ use Drupal\Core\Ajax\ReplaceCommand;
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;
@ -368,7 +369,7 @@ function _update_ckeditor5_html_filter(array $form, FormStateInterface $form_sta
/**
* 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
@ -413,7 +414,6 @@ function _ckeditor5_get_langcode_mapping($lang = FALSE) {
unset($langcodes[$langcode]);
}
}
if ($lang) {
return $langcodes[$lang] ?? 'en';
}
@ -537,7 +537,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';
@ -559,8 +559,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.

View File

@ -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', []);

View File

@ -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';

View File

@ -0,0 +1,224 @@
<?php
namespace Drupal\system\Controller;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
use Drupal\Core\Asset\AssetDumperUriInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\LibraryDependencyResolverInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Defines a controller to serve asset aggregates.
*/
abstract class AssetControllerBase extends FileDownloadController {
use AssetGroupSetHashTrait;
/**
* The asset type.
*
* @var string
*/
protected string $assetType;
/**
* The aggregate file extension.
*
* @var string
*/
protected string $fileExtension;
/**
* The asset aggregate content type to send as Content-Type header.
*
* @var string
*/
protected string $contentType;
/**
* The cache control header to use.
*
* 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.
*/
protected const CACHE_CONTROL = 'private, no-store';
/**
* Constructs an object derived from AssetControllerBase.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $libraryDependencyResolver
* The library dependency resolver.
* @param \Drupal\Core\Asset\AssetResolverInterface $assetResolver
* The asset resolver.
* @param \Drupal\Core\Theme\ThemeInitializationInterface $themeInitialization
* The theme initializer.
* @param \Drupal\Core\Theme\ThemeManagerInterface $themeManager
* The theme manager.
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
* The asset grouper.
* @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $optimizer
* The asset collection optimizer.
* @param \Drupal\Core\Asset\AssetDumperUriInterface $dumper
* The asset dumper.
*/
public function __construct(
StreamWrapperManagerInterface $streamWrapperManager,
protected readonly LibraryDependencyResolverInterface $libraryDependencyResolver,
protected readonly AssetResolverInterface $assetResolver,
protected readonly ThemeInitializationInterface $themeInitialization,
protected readonly ThemeManagerInterface $themeManager,
protected readonly AssetCollectionGrouperInterface $grouper,
protected readonly AssetCollectionOptimizerInterface $optimizer,
protected readonly AssetDumperUriInterface $dumper,
) {
parent::__construct($streamWrapperManager);
$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' => 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), 2);
// 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 (hash_equals($generated_hash, $received_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;
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\system\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
/**
* Defines a controller to serve CSS aggregates.
*/
class CssAssetController extends AssetControllerBase {
use AssetGroupSetHashTrait;
/**
* {@inheritdoc}
*/
protected string $contentType = 'text/css';
/**
* {@inheritdoc}
*/
protected string $assetType = 'css';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->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);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Drupal\system\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Asset\AssetGroupSetHashTrait;
/**
* Defines a controller to serve Javascript aggregates.
*/
class JsAssetController extends AssetControllerBase {
use AssetGroupSetHashTrait;
/**
* {@inheritdoc}
*/
protected string $contentType = 'application/javascript';
/**
* {@inheritdoc}
*/
protected string $assetType = 'js';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->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);
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Drupal\system\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
/**
* Defines a routes' callback to register an url for serving assets.
*/
class AssetRoutes implements ContainerInjectionInterface {
/**
* Constructs an asset routes object.
*
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager service.
*/
public function __construct(
protected readonly StreamWrapperManagerInterface $streamWrapperManager
) {}
/**
* {@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;
}
}

View File

@ -520,3 +520,6 @@ system.csrftoken:
_controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'
route_callbacks:
- '\Drupal\system\Routing\AssetRoutes::routes'

View File

@ -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)) {

View File

@ -52,12 +52,12 @@ class FrameworkTest extends BrowserTestBase {
$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 AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head');

View File

@ -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

View File

@ -0,0 +1,203 @@
<?php
namespace Drupal\FunctionalTests\Asset;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore abcdefghijklmnop
/**
* Tests asset aggregation.
*
* @group asset
*/
class AssetOptimizationTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['system'];
/**
* Tests that asset aggregates are rendered and created on disk.
*/
public function testAssetAggregation(): void {
$this->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, 2);
$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']);
}
}

View File

@ -63,8 +63,8 @@ class AttachedAssetsTest extends KernelTestBase {
*/
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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
@ -162,7 +162,7 @@ class AttachedAssetsTest extends KernelTestBase {
$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 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>';
@ -179,9 +179,9 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
];
// 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 @@ class AttachedAssetsTest extends KernelTestBase {
];
// 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 @@ class AttachedAssetsTest extends KernelTestBase {
$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 @@ class AttachedAssetsTest extends KernelTestBase {
// 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 @@ class AttachedAssetsTest extends KernelTestBase {
// 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 @@ class AttachedAssetsTest extends KernelTestBase {
$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);

View File

@ -11,6 +11,7 @@ use Drupal\Core\Asset\AssetResolver;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\UnitTestCase;
/**
@ -68,6 +69,16 @@ class AssetResolverTest extends UnitTestCase {
*/
protected $cache;
/**
* A mocked English language object.
*/
protected LanguageInterface $english;
/**
* A mocked Japanese language object.
*/
protected LanguageInterface $japanese;
/**
* {@inheritdoc}
*/
@ -95,10 +106,12 @@ class AssetResolverTest extends UnitTestCase {
$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 +126,8 @@ class AssetResolverTest extends UnitTestCase {
* @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 +136,12 @@ class AssetResolverTest extends UnitTestCase {
* @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());
}

View File

@ -0,0 +1,75 @@
<?php
namespace Drupal\Tests\Core\Asset;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
use Drupal\Core\Asset\AssetOptimizerInterface;
use Drupal\Core\Asset\LibraryDependencyResolverInterface;
use Drupal\Core\Asset\CssCollectionOptimizerLazy;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Tests the CSS asset optimizer.
*
* @group Asset
*/
class CssCollectionOptimizerLazyUnitTest extends UnitTestCase {
/**
* Tests that CSS imports with strange letters do not destroy the CSS output.
*/
public function testCssImport(): void {
$mock_grouper = $this->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);
$optimizer = new CssCollectionOptimizerLazy($mock_grouper, $mock_optimizer, $mock_theme_manager, $mock_dependency_resolver, new RequestStack(), $mock_file_system, $mock_config_factory, $mock_file_url_generator, $mock_time, $mock_language, $mock_state);
$aggregate = $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);
}
}

View File

@ -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 @@ class CssCollectionOptimizerUnitTest extends UnitTestCase {
});
$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 @@ class CssCollectionOptimizerUnitTest extends UnitTestCase {
'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);
}