diff --git a/core/core.services.yml b/core/core.services.yml index 6310c21..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', '@asset.css.dumper', '@state' ] + 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/AssetDumper.php b/core/lib/Drupal/Core/Asset/AssetDumper.php index 227ef0c..533f602 100644 --- a/core/lib/Drupal/Core/Asset/AssetDumper.php +++ b/core/lib/Drupal/Core/Asset/AssetDumper.php @@ -16,13 +16,15 @@ class AssetDumper implements AssetDumperInterface { * the aggregated contents of the files in $data. This forces proxies and * browsers to download new CSS when the CSS changes. */ - public function dump($data, $file_extension) { - // Prefix filename to prevent blocking by firewalls which reject files - // starting with "ad*". - $filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension; + public function dump($data, $file_extension, $uri = NULL) { // Create the css/ or js/ path within the files folder. $path = 'public://' . $file_extension; - $uri = $path . '/' . $filename; + if (!isset($uri)) { + // Prefix filename to prevent blocking by firewalls which reject files + // starting with "ad*". + $filename = $file_extension . '_' . Crypt::hashBase64($data) . '.' . $file_extension; + $uri = $path . '/' . $filename; + } // Create the CSS or JS file. file_prepare_directory($path, FILE_CREATE_DIRECTORY); if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) { diff --git a/core/lib/Drupal/Core/Asset/AssetDumperInterface.php b/core/lib/Drupal/Core/Asset/AssetDumperInterface.php index 2b53eb1..e7d0ac9 100644 --- a/core/lib/Drupal/Core/Asset/AssetDumperInterface.php +++ b/core/lib/Drupal/Core/Asset/AssetDumperInterface.php @@ -15,9 +15,12 @@ * @param string $file_extension * The file extension of this asset. * + * @param string $uri + * (optional) The file URI to write to. + * * @return string * An URI to access the dumped asset. */ - public function dump($data, $file_extension); + public function dump($data, $file_extension, $uri = NULL); } diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index 9e1bcce..003d88c 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -148,6 +148,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { // Always add a tiny value to the weight, to conserve the insertion // order. $options['weight'] += count($css) / 1000; + $options['library'] = $library; // CSS files are being keyed by the full path. $css[$options['data']] = $options; @@ -264,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 9beddd1..59ad098 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -2,7 +2,8 @@ namespace Drupal\Core\Asset; -use Drupal\Core\State\StateInterface; +use \Drupal\Component\Utility\UrlHelper; +use \Drupal\Core\Theme\ThemeManagerInterface; /** * Optimizes CSS assets. @@ -17,25 +18,11 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $grouper; /** - * A CSS asset optimizer. + * The theme manager. * - * @var \Drupal\Core\Asset\CssOptimizer + * @var \Drupal\Core\Theme\ThemeManagerInterface */ - protected $optimizer; - - /** - * An asset dumper. - * - * @var \Drupal\Core\Asset\AssetDumper - */ - protected $dumper; - - /** - * The state key/value store. - * - * @var \Drupal\Core\State\StateInterface - */ - protected $state; + protected $themeManager; /** * Constructs a CssCollectionOptimizer. @@ -44,16 +31,10 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { * The grouper for CSS assets. * @param \Drupal\Core\Asset\AssetOptimizerInterface * The optimizer for a single CSS asset. - * @param \Drupal\Core\Asset\AssetDumperInterface - * The dumper for optimized CSS assets. - * @param \Drupal\Core\State\StateInterface - * The state key/value store. */ - 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; } /** @@ -73,15 +54,10 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim public function optimize(array $css_assets) { // Group the assets. $css_groups = $this->grouper->group($css_assets); + $key = $this->generateHash($css_groups); - // Now optimize (concatenate + minify) and dump each asset group, unless - // that was already done, in which case it should appear in - // drupal_css_cache_files. - // Drupal contrib can override this default CSS 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('drupal_css_cache_files') ?: array(); $css_assets = array(); + $libraries = []; 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 @@ -96,38 +72,13 @@ public function optimize(array $css_assets) { $uri = $css_group['items'][0]['data']; $css_assets[$order]['data'] = $uri; } - // Preprocess (aggregate), unless the aggregate file already exists. else { - $key = $this->generateHash($css_group); - $uri = ''; - if (isset($map[$key])) { - $uri = $map[$key]; - } - if (empty($uri) || !file_exists($uri)) { - // Optimize each asset within the group. - $data = ''; - foreach ($css_group['items'] as $css_asset) { - $data .= $this->optimizer->optimize($css_asset); - } - // Per the W3C specification at - // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import - // rules must precede any other style, so we move those to the - // top. - $regexp = '/@import[^;]+;/i'; - preg_match_all($regexp, $data, $matches); - $data = preg_replace($regexp, '', $data); - $data = implode('', $matches[0]) . $data; - // Dump the optimized CSS for this group into an aggregate file. - $uri = $this->dumper->dump($data, 'css'); - // Set the URI for this group's aggregate file. - $css_assets[$order]['data'] = $uri; - // Persist the URI for this aggregate file. - $map[$key] = $uri; - $this->state->set('drupal_css_cache_files', $map); - } - else { - // Use the persisted URI for the optimized CSS file. - $css_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 ($css_group['items'] as $css_asset) { + $libraries[$css_asset['library']] = $css_asset['library']; } $css_assets[$order]['preprocessed'] = TRUE; } @@ -141,25 +92,44 @@ public function optimize(array $css_assets) { break; } } + // 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. + $uri = $path . '/' . $filename; + $css_assets[$order]['data'] = file_create_url($uri) . '?' . $query; + } + } return $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) { - $css_data = array(); - foreach ($css_group['items'] as $css_file) { - $css_data[] = $css_file['data']; + protected function generateHash(array $group) { + $normalized = $group; + foreach ($normalized as $order => $group) { + foreach ($group['items'] as $key => $asset) { + unset($normalized[$order]['items'][$key]['weight']); + } } - return hash('sha256', serialize($css_data)); + return hash('sha256', serialize($normalized)); } /** @@ -173,8 +143,6 @@ public function getAll() { * {@inheritdoc} */ public function deleteAll() { - $this->state->delete('drupal_css_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/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 new file mode 100644 index 0000000..1128485 --- /dev/null +++ b/core/modules/system/src/Controller/AssetControllerBase.php @@ -0,0 +1,280 @@ +libraryDependencyResolver = $library_dependency_resolver; + $this->assetResolver = $asset_resolver; + $this->themeInitialization = $theme_initialization; + $this->themeManager = $theme_manager; + $this->grouper = $grouper; + $this->optimizer = $optimizer; + $this->dumper = $dumper; + $this->fileExtension = $this->assetType; + } + + /** + * Generates a CSS aggregate, given a filename. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + * @param $file_name + * The file to deliver. + * + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Symfony\Component\HttpFoundation\Response + * The transferred file as response or some error response. + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * 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. + $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]; + + // 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 requrested 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. + // + // @todo: we could potentially hash the library definitions with the secret + // key to create an HMAC, so that only valid combinations of libraries are + // allowed. + // For now treat all of these the same, but if there's no match, don't write + // to the filesystem. + $match = TRUE; + + $uri = 'public://' . $this->assetType . '/' . $file_name; + if ($key !== $hash) { + // The file requested may have been written to disk by the time we got + // here. If it hasn't, and the hashes don't match, it's possible that a + // file matching the code base for this request already exists on disk, so + // use the filename matching this code base, not the one that generated + // the original filename for the request. + if (!file_exists($uri)) { + $uri = 'public://' . $this->assetType . '/' . $this->assetType . '_' . $group_delta . '_' . $hash . '.' . $this->fileExtension; + } + // Either way, we didn't get a match. + $match = FALSE; + } + $headers = [ + 'Content-Type' => $this->contentType, + // Headers sent from PHP can never perfectly match those sent when the + // file is served by the filesystem, so ensure this request does not get + // cached in either the browser or reverse proxies. Subsequent requests + // for the file will be served from disk and be cached. This is done to + // avoid situations such as where one CDN endpoint is serving a version + // cached from PHP, while another is serving a version cached from disk. + // Should there be any discrepancy in behaviour between those files, this + // can make debugging very difficult. + 'Cache-control' => $this->cacheControl, + ]; + + if (!file_exists($uri)) { + $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); + } + $headers = [ + 'Content-Type' => $this->mimeType, + 'Cache-control' => $this->cacheControl, + ]; + // Generate response. + $response = new Response($data, 200, $headers); + return $response; + } + else { + return new BinaryFileResponse($uri, 200, $headers); + } + } + + /** + * Generate a hash for a given group of assets. + * + * @param array $asset_group + * A group of assets. + * + * @return string + * A hash to uniquely identify the given group of assets. + */ + protected function generateHash(array $asset_group) { + $normalized = $asset_group; + foreach ($normalized as $order => $group) { + foreach ($group['items'] as $key => $asset) { + unset($normalized[$order]['items'][$key]['weight']); + } + } + return hash('sha256', serialize($normalized)); + } + + /** + * Get assets of a certain type given attached assets. + * + * @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 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 optimize($group); + +} diff --git a/core/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php new file mode 100644 index 0000000..6102521 --- /dev/null +++ b/core/modules/system/src/Controller/CssAssetController.php @@ -0,0 +1,78 @@ +contentType = 'text/css'; + $this->assetType = 'css'; + parent::__construct($library_dependency_resolver, $asset_resolver, $theme_initialization, $theme_manager, $grouper, $optimizer, $dumper); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $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.optimizer'), + $container->get('asset.css.dumper') + ); + } + + /** + * {@inheritdoc} + */ + 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 new file mode 100644 index 0000000..b44ef1b --- /dev/null +++ b/core/modules/system/src/Controller/JsAssetController.php @@ -0,0 +1,90 @@ +mimeType = 'application/javascript'; + $this->assetType = 'js'; + parent::__construct($library_dependency_resolver, $asset_resolver, $theme_initialization, $theme_manager, $grouper, $optimizer, $dumper); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $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.optimizer'), + $container->get('asset.js.dumper') + ); + } + + /** + * {@inheritdoc} + */ + 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); + } + + + +} diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php new file mode 100644 index 0000000..4a46446 --- /dev/null +++ b/core/modules/system/src/Routing/AssetRoutes.php @@ -0,0 +1,80 @@ +streamWrapperManager = $stream_wrapper_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('stream_wrapper_manager') + ); + } + + /** + * Returns an array of route objects. + * + * @return \Symfony\Component\Routing\Route[] + * An array of route objects. + */ + public function routes() { + $routes = array(); + // 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}', + array( + '_controller' => 'Drupal\system\Controller\CssAssetController::deliver', + ), + array( + '_access' => 'TRUE', + ) + ); + $routes['system.js_asset'] = new Route( + '/' . $directory_path . '/js/{file_name}', + array( + '_controller' => 'Drupal\system\Controller\JsAssetController::deliver', + ), + array( + '_access' => 'TRUE', + ) + ); + return $routes; + } + +} diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index ae89fb0..54accc5 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -507,3 +507,6 @@ system.csrftoken: _controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken' requirements: _access: 'TRUE' + +route_callbacks: + - '\Drupal\system\Routing\AssetRoutes::routes'