diff --git a/core/core.services.yml b/core/core.services.yml index 4d178eb5f9396eadb3c26c102e975d658be240d0..3f76a3e37e663a100855e6712ab1db15776401d8 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', '@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: diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Ajax/AjaxResponseAttachmentsProcessor.php index eaeed18ef2cac1b3992a92b97b4d6c8de3e23ef6..57228a0577cc70e955d2118cf2785588bcc42aa2 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; @@ -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 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor $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 @@ 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 0000000000000000000000000000000000000000..244013fc581482cc3e6f6cc1da67098cecf83377 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetCollectionGroupOptimizerInterface.php @@ -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; + +} diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php index 526acf50325120327333dc2b995c83d92799cfcb..80557a713feaaf52b43e9c41882977a620992a52 100644 --- a/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php +++ b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php @@ -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. diff --git a/core/lib/Drupal/Core/Asset/AssetDumper.php b/core/lib/Drupal/Core/Asset/AssetDumper.php index 6f90f8f5560f29331bbc8bff6f01c7722319db57..91360e5bc4642a132ab14afbcafda0aac1484507 100644 --- a/core/lib/Drupal/Core/Asset/AssetDumper.php +++ b/core/lib/Drupal/Core/Asset/AssetDumper.php @@ -9,7 +9,7 @@ /** * Dumps a CSS or JavaScript asset. */ -class AssetDumper implements AssetDumperInterface { +class AssetDumper implements AssetDumperUriInterface { /** * The file system service. @@ -36,12 +36,19 @@ public function __construct(FileSystemInterface $file_system) { * 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 { diff --git a/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..cf7085fef76fd3efc144515fc58c647bd6820201 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetDumperUriInterface.php @@ -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; + +} diff --git a/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php b/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..1d805b4c329be72f4bbae7f78b5ba87cd9494db8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetGroupSetHashTrait.php @@ -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()); + } + +} diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index 6f477d19f68eb9728c38da6be6c0f440f5266831..f4ca58bb3fb6ad5c7add95515998e4a45e6dd735 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -7,6 +7,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Theme\ThemeManagerInterface; /** @@ -109,12 +110,15 @@ protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { /** * {@inheritdoc} */ - public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { + public function getCssAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL) { + if (!isset($language)) { + $language = $this->languageManager->getCurrentLanguage(); + } $theme_info = $this->themeManager->getActiveTheme(); // Add the theme name to the cache key since themes may implement // hook_library_info_alter(). $libraries_to_load = $this->getLibrariesToLoad($assets); - $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; + $cid = 'css:' . $theme_info->getName() . ':' . $language->getId() . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize; if ($cached = $this->cache->get($cid)) { return $cached->data; } @@ -151,14 +155,14 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { } // Allow modules and themes to alter the CSS assets. - $this->moduleHandler->alter('css', $css, $assets); - $this->themeManager->alter('css', $css, $assets); + $this->moduleHandler->alter('css', $css, $assets, $language); + $this->themeManager->alter('css', $css, $assets, $language); // Sort CSS items, so that they appear in the correct order. uasort($css, [static::class, 'sort']); if ($optimize) { - $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); + $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css, $libraries_to_load, $language); } $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); @@ -194,13 +198,16 @@ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) { /** * {@inheritdoc} */ - public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { + public function getJsAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL) { + if (!isset($language)) { + $language = $this->languageManager->getCurrentLanguage(); + } $theme_info = $this->themeManager->getActiveTheme(); // Add the theme name to the cache key since themes may implement // hook_library_info_alter(). Additionally add the current language to // support translation of JavaScript files via hook_js_alter(). $libraries_to_load = $this->getLibrariesToLoad($assets); - $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; + $cid = 'js:' . $theme_info->getName() . ':' . $language->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize; if ($cached = $this->cache->get($cid)) { [$js_assets_header, $js_assets_footer, $settings, $settings_in_header] = $cached->data; @@ -258,8 +265,8 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { } // Allow modules and themes to alter the JavaScript assets. - $this->moduleHandler->alter('js', $javascript, $assets); - $this->themeManager->alter('js', $javascript, $assets); + $this->moduleHandler->alter('js', $javascript, $assets, $language); + $this->themeManager->alter('js', $javascript, $assets, $language); // Sort JavaScript assets, so that they appear in the correct order. uasort($javascript, [static::class, 'sort']); @@ -278,8 +285,8 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { if ($optimize) { $collection_optimizer = \Drupal::service('asset.js.collection_optimizer'); - $js_assets_header = $collection_optimizer->optimize($js_assets_header); - $js_assets_footer = $collection_optimizer->optimize($js_assets_footer); + $js_assets_header = $collection_optimizer->optimize($js_assets_header, $libraries_to_load); + $js_assets_footer = $collection_optimizer->optimize($js_assets_footer, $libraries_to_load); } // If the core/drupalSettings library is being loaded or is already diff --git a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php index 66f6ec095ab770b3953438dd1dfdd4c721d9d486..9100ebd57c75afe32c1143fe17be672bba58f5a9 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolverInterface.php +++ b/core/lib/Drupal/Core/Asset/AssetResolverInterface.php @@ -2,6 +2,8 @@ namespace Drupal\Core\Asset; +use Drupal\Core\Language\LanguageInterface; + /** * Resolves asset libraries into concrete CSS and JavaScript assets. * @@ -43,11 +45,13 @@ interface AssetResolverInterface { * @param bool $optimize * Whether to apply the CSS asset collection optimizer, to return an * optimized CSS asset collection rather than an unoptimized one. + * @param \Drupal\Core\Language\LanguageInterface $language + * (optional) The interface language the assets will be rendered with. * * @return array * A (possibly optimized) collection of CSS assets. */ - public function getCssAssets(AttachedAssetsInterface $assets, $optimize); + public function getCssAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL); /** * Returns the JavaScript assets for the current response's libraries. @@ -69,6 +73,8 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize); * @param bool $optimize * Whether to apply the JavaScript asset collection optimizer, to return * optimized JavaScript asset collections rather than an unoptimized ones. + * @param \Drupal\Core\Language\LanguageInterface $language + * (optional) The interface language for the assets will be rendered with. * * @return array * A nested array containing 2 values: @@ -77,6 +83,6 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize); * - at index one: the (possibly optimized) collection of JavaScript assets * for the bottom of the page */ - public function getJsAssets(AttachedAssetsInterface $assets, $optimize); + public function getJsAssets(AttachedAssetsInterface $assets, $optimize, LanguageInterface $language = NULL); } diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php index 7d47dad86e6ce688188f5f9a24647c06f9733524..8841866ff0975a6ae592484438b74f91ee0230ea 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -2,11 +2,18 @@ namespace Drupal\Core\Asset; +@trigger_error('The ' . __NAMESPACE__ . '\CssCollectionOptimizer is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead, use ' . __NAMESPACE__ . '\CssCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED); + use Drupal\Core\File\FileSystemInterface; use Drupal\Core\State\StateInterface; /** * Optimizes CSS assets. + * + * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead, use + * \Drupal\Core\Asset\CssCollectionOptimizerLazy. + * + * @see https://www.drupal.org/node/2888767 */ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { @@ -81,7 +88,7 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim * configurable period (@code system.performance.stale_file_threshold @endcode) * to ensure that files referenced by a cached page will still be available. */ - public function optimize(array $css_assets) { + public function optimize(array $css_assets, array $libraries) { // Group the assets. $css_groups = $this->grouper->group($css_assets); diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php new file mode 100644 index 0000000000000000000000000000000000000000..1c273438ca8684ca6d82cc9f1ec4a966b5f9a518 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php @@ -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; + } + +} diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index 99c8ac64879870201954788f2653549a080fb816..ceb8505c438fe490df7a960f3fb980edc9beefbb 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -2,11 +2,18 @@ namespace Drupal\Core\Asset; +@trigger_error('The ' . __NAMESPACE__ . '\JsCollectionOptimizer is deprecated in drupal:10.0.0 and is removed from drupal:11.0.0. Instead, use ' . __NAMESPACE__ . '\JsCollectionOptimizerLazy. See https://www.drupal.org/node/2888767', E_USER_DEPRECATED); + use Drupal\Core\File\FileSystemInterface; use Drupal\Core\State\StateInterface; /** * Optimizes JavaScript assets. + * + * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Instead use + * \Drupal\Core\Asset\JsCollectionOptimizerLazy. + * + * @see https://www.drupal.org/node/2888767 */ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { @@ -81,7 +88,7 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim * configurable period (@code system.performance.stale_file_threshold @endcode) * to ensure that files referenced by a cached page will still be available. */ - public function optimize(array $js_assets) { + public function optimize(array $js_assets, array $libraries) { // Group the assets. $js_groups = $this->grouper->group($js_assets); diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php new file mode 100644 index 0000000000000000000000000000000000000000..3a2a7137dc914c2262e434b3c85e058d1d9e5809 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -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); + } + +} diff --git a/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php b/core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php index 7df92bdb6db6436bcfb47151741eb14c0a6b7b29..c932f84181097bcc8fb79b95ae530a35ff3ba9b4 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; @@ -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 @@ public function __construct(AssetResolverInterface $asset_resolver, ConfigFactor $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 @@ 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 3ea1f6c44d6d6222f1fe6eee08671aa8b7302538..dc329fd6ef9013c497e8b3e4a76be18607f4d0fb 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 ff21df3367487a8d66a04bcec0b0d4d979c4eb34..09747e887fb8b47f1ff812bf6ab7b321d0f61db0 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 66ed363b6c0f1d0840ca6da1b8495c1440bd8e85..fe42a3e564684dacf888530ec28931d1574f8f90 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 2f77684e554459f628a403a121207fb5926600eb..f91a8bbeb26757f42f0f9fb2dfc3fe5a777329d9 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 0e43f969e499ff35af18cd0e04ee0f417cf9f4e6..eca98b75fed6ef1901f8818648c03df4f234252e 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 5be20aab8a7b374716e7a8a6f91b9912f183e3fd..1e0ed1de95cec193e070574000fa4a0b5d48764e 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; @@ -368,7 +369,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 @@ -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. diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 1a30d0bfc18456e252741cafcac91ba44abade75..7fbf1698d65e419d7dc0ecf06f08ca88d93ec09f 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 e16c213f267f8674fce597c6c472ce058633199d..0f3c3a262d97dd9de13df80e6610383262323add 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 0000000000000000000000000000000000000000..cb83318b391796e66fd9be16de59bc3b569c04d1 --- /dev/null +++ b/core/modules/system/src/Controller/AssetControllerBase.php @@ -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; + +} diff --git a/core/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php new file mode 100644 index 0000000000000000000000000000000000000000..76d1e8a8b7114a4d826e11f6dfc779a2dcfb8855 --- /dev/null +++ b/core/modules/system/src/Controller/CssAssetController.php @@ -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); + } + +} diff --git a/core/modules/system/src/Controller/JsAssetController.php b/core/modules/system/src/Controller/JsAssetController.php new file mode 100644 index 0000000000000000000000000000000000000000..7900286496b168ccadc4cf18a4b0fe9b3afd9805 --- /dev/null +++ b/core/modules/system/src/Controller/JsAssetController.php @@ -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); + } + +} diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php new file mode 100644 index 0000000000000000000000000000000000000000..b569824ec532b801c1891445c97e6e9282978eb5 --- /dev/null +++ b/core/modules/system/src/Routing/AssetRoutes.php @@ -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; + } + +} diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 00eddce6b5502a5e06dc5754bea2ffa47cb0d80e..362763fa18236877a5bed03ed5a292d906883afe 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 b38d5f7bbe9b1c76ef4610135d776649e3fcdc85..211147ef5b65536676c329880af4375f63c5ba01 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 669037f82d05f0c86c0f6ad2228cd1311e183aa8..5f44d545a514c969652ce51c477b2d44dcaa0aa7 100644 --- a/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php +++ b/core/modules/system/tests/src/Functional/Ajax/FrameworkTest.php @@ -52,12 +52,12 @@ public function testOrder() { $renderer = \Drupal::service('renderer'); $build['#attached']['library'][] = 'ajax_test/order-css-command'; $assets = AttachedAssets::createFromRenderArray($build); - $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE)); + $css_render_array = $css_collection_renderer->render($asset_resolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())); $expected_commands[1] = new AddCssCommand($renderer->renderRoot($css_render_array)); $build['#attached']['library'][] = 'ajax_test/order-header-js-command'; $build['#attached']['library'][] = 'ajax_test/order-footer-js-command'; $assets = AttachedAssets::createFromRenderArray($build); - [$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE); + [$js_assets_header, $js_assets_footer] = $asset_resolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); $js_header_render_array = $js_collection_renderer->render($js_assets_header); $js_footer_render_array = $js_collection_renderer->render($js_assets_footer); $expected_commands[2] = new AddJsCommand(array_column($js_header_render_array, '#attributes'), 'head'); diff --git a/core/phpstan-baseline.neon b/core/phpstan-baseline.neon index 50c770ed6213eac5c286174d234ebecf7f8dc1b1..3fe6bec6b1bd724037807f17dccf104bdd2f39aa 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 0000000000000000000000000000000000000000..c7b71cd84dcd4e96cfab1844ef060be72f4925b6 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Asset/AssetOptimizationTest.php @@ -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']); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php index 0a98220c141949313983057df80b15db33b11d29..26185c6a2c1af036153b3c16ee827a716ad84926 100644 --- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php @@ -63,8 +63,8 @@ protected function setUp(): void { */ public function testDefault() { $assets = new AttachedAssets(); - $this->assertEquals([], $this->assetResolver->getCssAssets($assets, FALSE), 'Default CSS is empty.'); - [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE); + $this->assertEquals([], $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()), 'Default CSS is empty.'); + [$js_assets_header, $js_assets_footer] = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); $this->assertEquals([], $js_assets_header, 'Default header JavaScript is empty.'); $this->assertEquals([], $js_assets_footer, 'Default footer JavaScript is empty.'); } @@ -76,7 +76,7 @@ public function testLibraryUnknown() { $build['#attached']['library'][] = 'core/unknown'; $assets = AttachedAssets::createFromRenderArray($build); - $this->assertSame([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.'); + $this->assertSame([], $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0], 'Unknown library was not added to the page.'); } /** @@ -86,8 +86,8 @@ public function testAddFiles() { $build['#attached']['library'][] = 'common_test/files'; $assets = AttachedAssets::createFromRenderArray($build); - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertArrayHasKey('core/modules/system/tests/modules/common_test/bar.css', $css); $this->assertArrayHasKey('core/modules/system/tests/modules/common_test/foo.js', $js); @@ -109,12 +109,12 @@ public function testAddJsSettings() { $assets = AttachedAssets::createFromRenderArray($build); $this->assertEquals([], $assets->getSettings(), 'JavaScript settings on $assets are empty.'); - $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertArrayHasKey('currentPath', $javascript['drupalSettings']['data']['path']); $this->assertArrayHasKey('currentPath', $assets->getSettings()['path']); $assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]); - $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $javascript = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertEquals(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.'); $this->assertEquals('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.'); } @@ -126,8 +126,8 @@ public function testAddExternalFiles() { $build['#attached']['library'][] = 'common_test/external'; $assets = AttachedAssets::createFromRenderArray($build); - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertArrayHasKey('http://example.com/stylesheet.css', $css); $this->assertArrayHasKey('http://example.com/script.js', $js); @@ -146,7 +146,7 @@ public function testAttributes() { $build['#attached']['library'][] = 'common_test/js-attributes'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $expected_1 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>'; @@ -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 = '<script src="http://example.com/deferred-external.js" foo="bar" defer></script>'; @@ -179,9 +179,9 @@ public function testAggregation() { $build['#attached']['library'][] = 'core/drupal.vertical-tabs'; $assets = AttachedAssets::createFromRenderArray($build); - $this->assertCount(1, $this->assetResolver->getCssAssets($assets, TRUE), 'There is a sole aggregated CSS asset.'); + $this->assertCount(1, $this->assetResolver->getCssAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage()), 'There is a sole aggregated CSS asset.'); - [$header_js, $footer_js] = $this->assetResolver->getJsAssets($assets, TRUE); + [$header_js, $footer_js] = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage()); $this->assertEquals([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.'); $rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js); $this->assertCount(2, $rendered_footer_js, 'There are 2 JavaScript assets in the footer.'); @@ -199,7 +199,7 @@ public function testSettings() { $build['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); // Cast to string since this returns a \Drupal\Core\Render\Markup object. $rendered_js = (string) $this->renderer->renderPlain($js_render_array); @@ -236,7 +236,7 @@ public function testHeaderHTML() { $build['#attached']['library'][] = 'common_test/js-header'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[0]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[0]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; @@ -252,7 +252,7 @@ public function testNoCache() { $build['#attached']['library'][] = 'common_test/no-cache'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertFalse($js['core/modules/system/tests/modules/common_test/nocache.js']['preprocess'], 'Setting cache to FALSE sets preprocess to FALSE when adding JavaScript.'); } @@ -263,7 +263,7 @@ public function testVersionQueryString() { $build['#attached']['library'][] = 'core/once'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); @@ -293,7 +293,7 @@ public function testRenderOrder() { ]; // Retrieve the rendered JavaScript and test against the regex. - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $matches = []; @@ -335,7 +335,7 @@ public function testRenderOrder() { ]; // Retrieve the rendered CSS and test against the regex. - $css = $this->assetResolver->getCssAssets($assets, FALSE); + $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); $rendered_css = $this->renderer->renderPlain($css_render_array); $matches = []; @@ -358,7 +358,7 @@ public function testRenderDifferentWeight() { $build['#attached']['library'][] = 'common_test/weight'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); // Verify that lighter CSS assets are rendered first. @@ -383,7 +383,7 @@ public function testAlter() { // Render the JavaScript, testing if alter.js was altered to be before // tableselect.js. See common_test_js_alter() to see where this alteration // takes place. - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); // Verify that JavaScript weight is correctly altered by the alter hook. @@ -405,7 +405,7 @@ public function testLibraryAlter() { // common_test_library_info_alter() also added a dependency on jQuery Form. $build['#attached']['library'][] = 'core/jquery.farbtastic'; $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); $rendered_js = $this->renderer->renderPlain($js_render_array); $this->assertStringContainsString('core/assets/vendor/jquery-form/jquery.form.min.js', (string) $rendered_js, 'Altered library dependencies are added to the page.'); @@ -450,8 +450,8 @@ public function testAddJsFileWithQueryString() { $build['#attached']['library'][] = 'common_test/querystring'; $assets = AttachedAssets::createFromRenderArray($build); - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $css = $this->assetResolver->getCssAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage()); + $js = $this->assetResolver->getJsAssets($assets, FALSE, \Drupal::languageManager()->getCurrentLanguage())[1]; $this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css); $this->assertArrayHasKey('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js); diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php index f9f58764db8bcf28be9c000c160a0187f94a81f9..b2589385abfa6e5dfa41eb3bea0c30fec30d2691 100644 --- a/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/AssetResolverTest.php @@ -11,6 +11,7 @@ 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 @@ protected function setUp(): void { $english->expects($this->any()) ->method('getId') ->willReturn('en'); + $this->english = $english; $japanese = $this->createMock('\Drupal\Core\Language\LanguageInterface'); $japanese->expects($this->any()) ->method('getId') ->willReturn('jp'); + $this->japanese = $japanese; $this->languageManager = $this->createMock('\Drupal\Core\Language\LanguageManagerInterface'); $this->languageManager->expects($this->any()) ->method('getCurrentLanguage') @@ -113,8 +126,8 @@ protected function setUp(): void { * @dataProvider providerAttachedAssets */ public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) { - $this->assetResolver->getCssAssets($assets_a, FALSE); - $this->assetResolver->getCssAssets($assets_b, FALSE); + $this->assetResolver->getCssAssets($assets_a, FALSE, $this->english); + $this->assetResolver->getCssAssets($assets_b, FALSE, $this->english); $this->assertCount($expected_cache_item_count, $this->cache->getAllCids()); } @@ -123,12 +136,12 @@ public function testGetCssAssets(AttachedAssetsInterface $assets_a, AttachedAsse * @dataProvider providerAttachedAssets */ public function testGetJsAssets(AttachedAssetsInterface $assets_a, AttachedAssetsInterface $assets_b, $expected_cache_item_count) { - $this->assetResolver->getJsAssets($assets_a, FALSE); - $this->assetResolver->getJsAssets($assets_b, FALSE); + $this->assetResolver->getJsAssets($assets_a, FALSE, $this->english); + $this->assetResolver->getJsAssets($assets_b, FALSE, $this->english); $this->assertCount($expected_cache_item_count, $this->cache->getAllCids()); - $this->assetResolver->getJsAssets($assets_a, FALSE); - $this->assetResolver->getJsAssets($assets_b, FALSE); + $this->assetResolver->getJsAssets($assets_a, FALSE, $this->japanese); + $this->assetResolver->getJsAssets($assets_b, FALSE, $this->japanese); $this->assertCount($expected_cache_item_count * 2, $this->cache->getAllCids()); } diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8e58467ea885b7262589acc26ad41a049e5eea2d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php @@ -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); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php index a27ce061978067d4a6ceb8541a321f4b7a6dc43a..f08dd9e1e310c65f77e85ed5f918665d539ef588 100644 --- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerUnitTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\Core\Asset; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Asset\AssetCollectionGrouperInterface; use Drupal\Core\Asset\AssetDumperInterface; use Drupal\Core\Asset\AssetOptimizerInterface; @@ -31,8 +32,12 @@ class CssCollectionOptimizerUnitTest extends UnitTestCase { */ protected $optimizer; - protected function setUp(): void { - parent::setUp(); + /** + * Tests that CSS imports with strange letters do not destroy the CSS output. + * + * @group legacy + */ + public function testCssImport() { $mock_grouper = $this->createMock(AssetCollectionGrouperInterface::class); $mock_grouper->method('group') ->willReturnCallback(function ($assets) { @@ -57,13 +62,8 @@ protected function setUp(): void { }); $mock_state = $this->createMock(StateInterface::class); $mock_file_system = $this->createMock(FileSystemInterface::class); - $this->optimizer = new CssCollectionOptimizer($mock_grouper, $mock_optimizer, $mock_dumper, $mock_state, $mock_file_system); - } - - /** - * Test that css imports with strange letters do not destroy the css output. - */ - public function testCssImport() { + $mock_time = $this->createMock(TimeInterface::class); + $this->optimizer = new CssCollectionOptimizer($mock_grouper, $mock_optimizer, $mock_dumper, $mock_state, $mock_file_system, $mock_time); $this->optimizer->optimize([ 'core/modules/system/tests/modules/common_test/common_test_css_import.css' => [ 'type' => 'file', @@ -75,7 +75,8 @@ public function testCssImport() { 'data' => 'core/modules/system/tests/modules/common_test/common_test_css_import.css', 'preprocess' => TRUE, ], - ]); + ], + []); self::assertEquals(file_get_contents(__DIR__ . '/css_test_files/css_input_with_import.css.optimized.aggregated.css'), $this->dumperData); }