diff --git a/core/core.services.yml b/core/core.services.yml index b573a6d..6ac951f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1500,7 +1500,7 @@ services: arguments: [ '@state' ] asset.css.collection_optimizer: class: Drupal\Core\Asset\CssCollectionOptimizer - arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager' ] + arguments: [ '@asset.css.collection_grouper', '@theme.manager' ] asset.css.optimizer: class: Drupal\Core\Asset\CssOptimizer asset.css.collection_grouper: @@ -1512,7 +1512,7 @@ services: arguments: [ '@state' ] asset.js.collection_optimizer: class: Drupal\Core\Asset\JsCollectionOptimizer - arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state' ] + arguments: [ '@asset.js.collection_grouper', '@theme.manager' ] asset.js.optimizer: class: Drupal\Core\Asset\JsOptimizer asset.js.collection_grouper: @@ -1535,7 +1535,7 @@ services: arguments: ['@library.discovery'] asset.resolver: class: Drupal\Core\Asset\AssetResolver - arguments: ['@library.discovery', '@library.dependency_resolver', '@module_handler', '@theme.manager', '@language_manager', '@cache.data'] + arguments: ['@library.discovery', '@library.dependency_resolver', '@module_handler', '@theme.manager', '@language_manager', '@cache.data', '@asset.css.collection_optimizer', '@asset.js.collection_optimizer'] info_parser: class: Drupal\Core\Extension\InfoParser twig: diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index 1a079ca..003d88c 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -118,24 +118,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { if ($cached = $this->cache->get($cid)) { return $cached->data; } - else { - $css = $this->getCssFromLibraries($libraries_to_load, $optimize); - } - if ($optimize) { - $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); - } - $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); - - return $css; - - } - /** - * Get css assets based on a list of libraries. - * @todo: interface. - */ - public function getCssFromLibraries($libraries, $optimize) { - $theme_info = $this->themeManager->getActiveTheme(); $css = []; $default_options = [ 'type' => 'file', @@ -146,7 +129,7 @@ public function getCssFromLibraries($libraries, $optimize) { 'browsers' => [], ]; - foreach ($libraries as $library) { + foreach ($libraries_to_load as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['css'])) { @@ -190,6 +173,11 @@ public function getCssFromLibraries($libraries, $optimize) { } } + if ($optimize) { + $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); + } + $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); + return $css; } @@ -277,6 +265,7 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize) { // Always add a tiny value to the weight, to conserve the insertion // order. $options['weight'] += count($javascript) / 1000; + $options['library'] = $library; // Local and external files must keep their name as the associative // key so the same JavaScript file is not added twice. diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php index 2e20fb7..59ad098 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Asset; -use Drupal\Core\State\StateInterface; use \Drupal\Component\Utility\UrlHelper; use \Drupal\Core\Theme\ThemeManagerInterface; @@ -19,13 +18,6 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $grouper; /** - * A CSS asset optimizer. - * - * @var \Drupal\Core\Asset\CssOptimizer - */ - protected $optimizer; - - /** * The theme manager. * * @var \Drupal\Core\Theme\ThemeManagerInterface @@ -40,9 +32,8 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { * @param \Drupal\Core\Asset\AssetOptimizerInterface * The optimizer for a single CSS asset. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager) { + public function __construct(AssetCollectionGrouperInterface $grouper, ThemeManagerInterface $theme_manager) { $this->grouper = $grouper; - $this->optimizer = $optimizer; $this->themeManager = $theme_manager; } @@ -104,15 +95,17 @@ public function optimize(array $css_assets) { // Generate a URL for the group, but do not process it inline, this is // done by \Drupal\system\controller\CssAssetController $minimal_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($libraries); - + $theme_name = $this->themeManager->getActiveTheme()->getName(); + $ajax_page_state = \Drupal::request()->get('ajax_page_state'); + $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []; + $minimal_already_loaded_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($already_loaded); + $query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set]); + $path = 'public://' . 'css'; foreach ($css_assets as $order => $css_asset) { if (!empty($css_asset['preprocessed'])) { $filename = 'css' . '_' . $order . '_' . $key . '.' . 'css'; // Create the css/ or js/ path within the files folder. - $path = 'public://' . 'css'; $uri = $path . '/' . $filename; - $theme_name = $this->themeManager->getActiveTheme()->getName(); - $query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name]); $css_assets[$order]['data'] = file_create_url($uri) . '?' . $query; } } @@ -121,16 +114,16 @@ public function optimize(array $css_assets) { } /** - * Generate a hash for a given group of CSS assets. + * Generate a hash for a given group of assets. * - * @param array $css_group - * A group of CSS assets. + * @param array $group + * A group of assets. * * @return string - * A hash to uniquely identify the given group of CSS assets. + * A hash to uniquely identify the given group of assets. */ - protected function generateHash(array $css_group) { - $normalized = $css_group; + protected function generateHash(array $group) { + $normalized = $group; foreach ($normalized as $order => $group) { foreach ($group['items'] as $key => $asset) { unset($normalized[$order]['items'][$key]['weight']); diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index aef10bf..c35dd7b 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -2,8 +2,8 @@ namespace Drupal\Core\Asset; -use Drupal\Core\State\StateInterface; - +use \Drupal\Component\Utility\UrlHelper; +use \Drupal\Core\Theme\ThemeManagerInterface; /** * Optimizes JavaScript assets. @@ -18,58 +18,34 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $grouper; /** - * A JS asset optimizer. - * - * @var \Drupal\Core\Asset\JsOptimizer - */ - protected $optimizer; - - /** - * An asset dumper. - * - * @var \Drupal\Core\Asset\AssetDumper - */ - protected $dumper; - - /** - * The state key/value store. + * The theme manager. * - * @var \Drupal\Core\State\StateInterface + * @var \Drupal\Core\Theme\ThemeManagerInterface */ - protected $state; + protected $themeManager; /** * Constructs a JsCollectionOptimizer. * * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface * The grouper for JS assets. - * @param \Drupal\Core\Asset\AssetOptimizerInterface - * The optimizer for a single JS asset. - * @param \Drupal\Core\Asset\AssetDumperInterface - * The dumper for optimized JS assets. - * @param \Drupal\Core\State\StateInterface - * The state key/value store. + * @param \Drupal\Core\Theme\ThemeManagerInterface; + * The theme manger. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) { + public function __construct(AssetCollectionGrouperInterface $grouper, ThemeManagerInterface $theme_manager) { $this->grouper = $grouper; - $this->optimizer = $optimizer; - $this->dumper = $dumper; - $this->state = $state; + $this->themeManager = $theme_manager; } /** * {@inheritdoc} * - * The cache file name is retrieved on a page load via a lookup variable that - * contains an associative array. The array key is the hash of the names in - * $files while the value is the cache file name. The cache file is generated - * in two cases. First, if there is no file name value for the key, which will - * happen if a new file name has been added to $files or after the lookup - * variable is emptied to force a rebuild of the cache. Second, the cache file - * is generated if it is missing on disk. Old cache files are not deleted - * immediately when the lookup variable is emptied, but are deleted after a - * configurable period (@code system.performance.stale_file_threshold @endcode) - * to ensure that files referenced by a cached page will still be available. + * File names are generated based on library/asset definitions. This includes + * a hash of the assets and the group index. Additionally the full set of + * libraries, already loaded libraries and theme are sent as query parameters + * to allow a PHP controller to generate a valid file with sufficient + * information. Files are not generated by this method since they're assumed + * to be successfully returned from the URL created whether on disk or not. */ public function optimize(array $js_assets) { // Group the assets. @@ -81,7 +57,6 @@ public function optimize(array $js_assets) { // Drupal contrib can override this default JS aggregator to keep the same // grouping, optimizing and dumping, but change the strategy that is used to // determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …). - $map = $this->state->get('system.js_cache_files') ?: array(); $js_assets = array(); foreach ($js_groups as $order => $js_group) { // We have to return a single asset, not a group of assets. It is now up @@ -97,41 +72,13 @@ public function optimize(array $js_assets) { $uri = $js_group['items'][0]['data']; $js_assets[$order]['data'] = $uri; } - // Preprocess (aggregate), unless the aggregate file already exists. else { - $key = $this->generateHash($js_group); - $uri = ''; - if (isset($map[$key])) { - $uri = $map[$key]; - } - if (empty($uri) || !file_exists($uri)) { - // Concatenate each asset within the group. - $data = ''; - foreach ($js_group['items'] as $js_asset) { - // Optimize this JS file, but only if it's not yet minified. - if (isset($js_asset['minified']) && $js_asset['minified']) { - $data .= file_get_contents($js_asset['data']); - } - else { - $data .= $this->optimizer->optimize($js_asset); - } - // Append a ';' and a newline after each JS file to prevent them - // from running together. - $data .= ";\n"; - } - // Remove unwanted JS code that cause issues. - $data = $this->optimizer->clean($data); - // Dump the optimized JS for this group into an aggregate file. - $uri = $this->dumper->dump($data, 'js'); - // Set the URI for this group's aggregate file. - $js_assets[$order]['data'] = $uri; - // Persist the URI for this aggregate file. - $map[$key] = $uri; - $this->state->set('system.js_cache_files', $map); - } - else { - // Use the persisted URI for the optimized JS file. - $js_assets[$order]['data'] = $uri; + // 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. + foreach ($js_group['items'] as $js_asset) { + $libraries[$js_asset['library']] = $js_asset['library']; } $js_assets[$order]['preprocessed'] = TRUE; } @@ -146,6 +93,25 @@ public function optimize(array $js_assets) { } } + // Generate a URL for the group, but do not process it inline, this is + // done by \Drupal\system\controller\JsAssetController + $minimal_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($libraries); + $theme_name = $this->themeManager->getActiveTheme()->getName(); + $ajax_page_state = \Drupal::request()->get('ajax_page_state'); + $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []; + $minimal_already_loaded_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($already_loaded); + $header_query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set, 'scope' => 'header']); + $footer_query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set, 'scope' => 'footer']); + $path = 'public://' . 'js'; + foreach ($js_assets as $order => $js_asset) { + if (!empty($js_asset['preprocessed'])) { + $query = $js_asset['scope'] == 'header' ? $header_query : $footer_query; + $filename = 'js' . '_' . $order . '_' . $key . '.' . 'js'; + $uri = $path . '/' . $filename; + $js_assets[$order]['data'] = file_create_url($uri) . '?' . $query; + } + } + return $js_assets; } @@ -170,14 +136,14 @@ protected function generateHash(array $js_group) { * {@inheritdoc} */ public function getAll() { - return $this->state->get('system.js_cache_files'); + // @todo: whut? + return []; } /** * {@inheritdoc} */ public function deleteAll() { - $this->state->delete('system.js_cache_files'); $delete_stale = function($uri) { // Default stale file threshold is 30 days. if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { diff --git a/core/modules/system/src/Controller/AssetControllerBase.php b/core/modules/system/src/Controller/AssetControllerBase.php index 3cc6cc7..1128485 100644 --- a/core/modules/system/src/Controller/AssetControllerBase.php +++ b/core/modules/system/src/Controller/AssetControllerBase.php @@ -5,6 +5,8 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Asset\AssetCollectionGrouperInterface; use Drupal\Core\Asset\AssetOptimizerInterface; +use Drupal\Core\Asset\AttachedAssets; +use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Theme\ThemeInitializationInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\system\FileDownloadController; @@ -139,48 +141,34 @@ public function __construct($library_dependency_resolver, $asset_resolver, $them * Thrown when the filename is invalid. */ public function deliver(Request $request, $file_name) { + if (!$request->query->has('theme')) { + throw new BadRequestHttpException('The theme must be passed as a query argument'); + } + + $file_parts = explode('_', basename($file_name, '.' . $this->fileExtension)); + // The group delta is the second segment of the filename and the hash is the + // third segment. If either are not there, then the filename is invalid. + if (!isset($file_parts[1]) || !is_numeric($file_parts[1]) || !isset($file_parts[2])) { + throw new BadRequestHttpException('Invalid filename'); + } + $group_delta = $file_parts[1]; + $theme = $request->query->get('theme'); $active_theme = $this->themeInitialization->initTheme($theme); $this->themeManager->setActiveTheme($active_theme); // While the libraries are taken from the query parameter, the URL as a // whole is validated against a hash of the CSS assets later on. - $libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($request->query->get('libraries')); - $assets = $this->assetResolver->getCssFromLibraries($libraries, TRUE); - - // Group the assets. - $groups = $this->grouper->group($assets); - $libraries = []; - - $base_name = basename($file_name, '.' . $this->fileExtension); - - $file_parts = explode('_', $base_name); - // The group delta is the second segment of the filename, if it's not there - // then the filename is invalid. - if (!isset($file_parts[1]) || !is_numeric($file_parts[1])) { - throw new BadRequestHttpException('Invalid filename'); - } - $group_delta = $file_parts[1]; - - // The hash is the third segment of the filename. - if (!isset($file_parts[2])) { - throw new BadRequestHttpException('Invalid filename'); + $attached_assets = new AttachedAssets(); + $attached_assets->setLibraries($request->query->get('libraries')); + if ($request->query->has('already_loaded')) { + $attached_assets->setAlreadyLoadedLibraries($request->query->get('already_loaded')); } + $key_and_group = $this->getKeyAndGroup($attached_assets, $group_delta, $request); + $key = $key_and_group['key']; + $group = $key_and_group['group']; $hash = $file_parts[2]; - // If the group being requested does not exist, assume an invalid filename. - if (!isset($groups[$group_delta])) { - throw new BadRequestHttpException('Invalid filename'); - } - $group = $groups[$group_delta]; - - // Only groups that are preprocessed will be requested, so don't try to - // process ones that aren't. - if (!$group['preprocess']) { - throw new BadRequestHttpException('Invalid filename'); - } - $key = $this->generateHash($groups); - // 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 @@ -224,19 +212,7 @@ public function deliver(Request $request, $file_name) { ]; if (!file_exists($uri)) { - // Optimize each asset within the group. - $data = ''; - foreach ($group['items'] as $asset) { - $data .= $this->optimizer->optimize($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. - $regexp = '/@import[^;]+;/i'; - preg_match_all($regexp, $data, $matches); - $data = preg_replace($regexp, '', $data); - $data = implode('', $matches[0]) . $data; + $data = $this->optimize($group); // Dump the optimized CSS for this group into an aggregate file. if ($match) { $uri = $this->dumper->dump($data, $this->assetType, $uri); @@ -274,14 +250,31 @@ protected function generateHash(array $asset_group) { } /** - * Get assets of a certain type given libraries. + * Get assets of a certain type given attached assets. * - * @param $libraries - * An array of library definitions. + * @param AttachedAssetsInterface $attached_assets + * An object implementing AttachedAssetsInterface + * @param int $group_delta + * The group delta. + * @param Symfony\Component\HttpFoundation\Request; + * The current request. * * @return [] - * An array of assets. + * An array with 'key' and 'group' as keys, the hashed group as the value + * for key and the individual group matching $group_delta as the value for + * group. + */ + abstract protected function getKeyAndGroup(AttachedAssetsInterface $attached_assets, $group_delta, $request); + + /** + * Optimize a group in preparation for serving an aggregate file. + * + * @param array $group + * A group of assets. + * + * @return string + * An optimized string based on the contents of the group. */ - abstract protected function getAssetsFromLibraries($libraries); + abstract protected function optimize($group); } diff --git a/core/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php index 87e2b7b..6102521 100644 --- a/core/modules/system/src/Controller/CssAssetController.php +++ b/core/modules/system/src/Controller/CssAssetController.php @@ -3,6 +3,10 @@ namespace Drupal\system\Controller; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + + +use Drupal\Core\Asset\AttachedAssetsInterface; /** * Defines a controller to serve CSS aggregates. @@ -13,7 +17,7 @@ class CssAssetController extends AssetControllerBase { * {@inheritdoc} */ public function __construct($library_dependency_resolver, $asset_resolver, $theme_initialization, $theme_manager, $grouper, $optimizer, $dumper) { - $this->mimeType = 'text/css'; + $this->contentType = 'text/css'; $this->assetType = 'css'; parent::__construct($library_dependency_resolver, $asset_resolver, $theme_initialization, $theme_manager, $grouper, $optimizer, $dumper); } @@ -36,8 +40,39 @@ public static function create(ContainerInterface $container) { /** * {@inheritdoc} */ - protected function getAssetsFromLibraries($libraries) { - return $this->assetResolver->getCssAssets($libraries, TRUE); + protected function getKeyAndGroup(AttachedAssetsInterface $attached_assets, $group_delta, $request) { + $assets = $this->assetResolver->getCssAssets($attached_assets, FALSE); + + // Group the assets. + $groups = $this->grouper->group($assets); + if (!isset($groups[$group_delta]['preprocess'])) { + throw new BadRequestHttpException('Invalid filename.'); + } + $group = $groups[$group_delta]; + $key = $this->generateHash($groups); + return [ + 'group' => $group, + 'key' => $key, + ]; + } + + /** + * {@inheritdoc} + */ + protected function optimize($group) { + // Optimize each asset within the group. + $data = ''; + foreach ($group['items'] as $asset) { + $data .= $this->optimizer->optimize($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. + $regexp = '/@import[^;]+;/i'; + preg_match_all($regexp, $data, $matches); + $data = preg_replace($regexp, '', $data); + return implode('', $matches[0]) . $data; } } diff --git a/core/modules/system/src/Controller/JsAssetController.php b/core/modules/system/src/Controller/JsAssetController.php index d166c91..b44ef1b 100644 --- a/core/modules/system/src/Controller/JsAssetController.php +++ b/core/modules/system/src/Controller/JsAssetController.php @@ -3,6 +3,9 @@ namespace Drupal\system\Controller; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + +use Drupal\Core\Asset\AttachedAssetsInterface; /** * Defines a controller to serve Javascript aggregates. @@ -36,8 +39,52 @@ public static function create(ContainerInterface $container) { /** * {@inheritdoc} */ - protected function getAssetsFromLibraries($libraries) { - return $this->assetResolver->getJsAssets($libraries, TRUE); + protected function getKeyAndGroup(AttachedAssetsInterface $attached_assets, $group_delta, $request) { + // 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. + list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($attached_assets, FALSE); + $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; + // Group the assets. + $groups = $this->grouper->group($assets); + if (!isset($groups[$group_delta]['preprocess'])) { + throw new BadRequestHttpException('Invalid filename.'); + } + $group = $groups[$group_delta]; + $key = $this->generateHash($groups); + return [ + 'group' => $group, + 'key' => $key, + ]; + + return []; } + /** + * {@inheritdoc} + */ + protected function optimize($group) { + $data = ''; + foreach ($group['items'] as $js_asset) { + // Optimize this JS file, but only if it's not yet minified. + if (isset($js_asset['minified']) && $js_asset['minified']) { + $data .= file_get_contents($js_asset['data']); + } + else { + $data .= $this->optimizer->optimize($js_asset); + } + // Append a ';' and a newline after each JS file to prevent them + // from running together. + $data .= ";\n"; + } + // Remove unwanted JS code that cause issues. + return $this->optimizer->clean($data); + } + + + }