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