diff --git a/core/core.services.yml b/core/core.services.yml index c5a13a4..7888005 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1445,7 +1445,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', '@asset.css.optimizer', '@theme.manager' ] asset.css.optimizer: class: Drupal\Core\Asset\CssOptimizer asset.css.collection_grouper: diff --git a/core/lib/Drupal/Core/Asset/AssetResolver.php b/core/lib/Drupal/Core/Asset/AssetResolver.php index 8b021b7..c05e5f6 100644 --- a/core/lib/Drupal/Core/Asset/AssetResolver.php +++ b/core/lib/Drupal/Core/Asset/AssetResolver.php @@ -122,7 +122,24 @@ 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', @@ -133,7 +150,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { 'browsers' => [], ]; - foreach ($libraries_to_load as $library) { + foreach ($libraries as $library) { list($extension, $name) = explode('/', $library, 2); $definition = $this->libraryDiscovery->getLibraryByName($extension, $name); if (isset($definition['css'])) { @@ -177,11 +194,6 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) { } } - if ($optimize) { - $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css); - } - $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']); - return $css; } diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php index d804113..df9a104 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -9,6 +9,7 @@ use Drupal\Core\State\StateInterface; use \Drupal\Component\Utility\UrlHelper; +use \Drupal\Core\Theme\ThemeManagerInterface; /** * Optimizes CSS assets. @@ -30,18 +31,11 @@ class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $optimizer; /** - * An asset dumper. + * The theme manager. * - * @var \Drupal\Core\Asset\AssetDumper + * @var \Drupal\Core\Theme\ThemeManagerInterface */ - protected $dumper; - - /** - * The state key/value store. - * - * @var \Drupal\Core\State\StateInterface - */ - protected $state; + protected $themeManager; /** * Constructs a CssCollectionOptimizer. @@ -50,16 +44,11 @@ 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, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager) { $this->grouper = $grouper; $this->optimizer = $optimizer; - $this->dumper = $dumper; - $this->state = $state; + $this->themeManager = $theme_manager; } /** @@ -80,13 +69,6 @@ public function optimize(array $css_assets) { // Group the assets. $css_groups = $this->grouper->group($css_assets); - // 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) { @@ -103,7 +85,6 @@ 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 { // To reproduce the full context of assets outside of the request, // we must know the entire set of libraries used to generate all CSS @@ -112,37 +93,6 @@ public function optimize(array $css_assets) { foreach ($css_group['items'] as $css_asset) { $libraries[$css_asset['library']] = $css_asset['library']; } - $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; - } $css_assets[$order]['preprocessed'] = TRUE; } break; @@ -165,17 +115,19 @@ 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); foreach ($css_assets as $order => $css_asset) { if (!empty($css_asset['preprocessed'])) { + $key = $this->generateHash($css_asset); $filename = 'css' . '_' . $order . '_' . $key . '.' . 'css'; // Create the css/ or js/ path within the files folder. - $path = 'public://' . 'assets/css'; + $path = 'public://' . 'css'; $uri = $path . '/' . $filename; - $array_key = $order . '-test'; - $css_assets[$array_key] = $css_asset; - $query = UrlHelper::buildQuery(['libraries' => $minimal_set]); - $css_assets[$array_key]['data'] = $uri . '?=' . $query; + $theme_name = $this->themeManager->getActiveTheme()->getName(); + $query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name]); + $css_assets[$order]['data'] = file_create_url($uri) . '?=' . $query; } } @@ -206,8 +158,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/modules/system/src/Controller/CssAssetController.php b/core/modules/system/src/Controller/CssAssetController.php new file mode 100644 index 0000000..5530af1 --- /dev/null +++ b/core/modules/system/src/Controller/CssAssetController.php @@ -0,0 +1,254 @@ +libraryDependencyResolver = $library_dependency_resolver; + $this->assetResolver = $asset_resolver; + $this->themeInitialization = $theme_initialization; + $this->themeManager = $theme_manager; + $this->grouper = $grouper; + $this->optimizer = $optimizer; + } + + /** + * {@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') + ); + } + + /** + * Generates a derivative, given a style and image path. + * + * After generating an image, transfer it to the requesting agent. + * + * @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\AccessDeniedHttpException + * Thrown when the user does not have access to the file. + * @throws \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException + * Thrown when the file is still being generated. + */ + public function deliver(Request $request, $file_name) { + $theme = $request->query->get('theme'); + $active_theme = $this->themeInitialization->initTheme($theme); + $this->themeManager->setActiveTheme($active_theme); + $libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($request->query->get('libraries')); + $css_assets = $this->assetResolver->getCssFromLibraries($libraries, TRUE); + + // Group the assets. + $css_groups = $this->grouper->group($css_assets); + + // 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 …). + $css_assets = array(); + $libraries = []; + + $base_name = basename($file_name); + + $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. + // A non-matching hash does not necessarily mean a bad request, it could + // also mean that code is temporarily out of sync between different servers. + if (!isset($file_parts[2])) { + throw new BadRequestHttpException('Invalid filename'); + } + $hash = $file_parts[2]; + + + $css_group = $css_groups[$group_delta]; + + // Only groups that are preprocessed will be requested, so don't try to + // process ones that aren't. + if (!$css_group['preprocess']) { + throw new BadRequestHttpException('Invalid filename'); + } + $key = $this->generateHash($css_group); + + + // 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://css/' . $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://css/' . '_' . $group_delta . '_' . $hash . '.css'; + } + // Either way, we didn't get a match. + $match = FALSE; + } + $headers = [ + 'Content-Type' => 'text/css', + 'Cache-control' => 'private, no-store', + ]; + + if (!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. + if ($match) { + $uri = $this->dumper->dump($data, 'css'); + } + $headers = [ + 'Content-Type' => 'text/css', + 'Cache-control' => 'private, no-store', + ]; + // 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 CSS assets. + * + * @param array $css_group + * A group of CSS assets. + * + * @return string + * A hash to uniquely identify the given group of CSS assets. + */ + protected function generateHash(array $css_group) { + return hash('sha256', serialize($css_group)); + } + +} diff --git a/core/modules/system/src/Routing/AssetRoutes.php b/core/modules/system/src/Routing/AssetRoutes.php new file mode 100644 index 0000000..841fc67 --- /dev/null +++ b/core/modules/system/src/Routing/AssetRoutes.php @@ -0,0 +1,71 @@ +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', + ) + ); + return $routes; + } + +}