Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetCollectionGrouperInterface.
*/
namespace Drupal\Core\Asset;
/**
* Interface defining a service that logically groups a collection of assets.
*/
interface AssetCollectionGrouperInterface {
/**
* Groups a collection of assets into logical groups of asset collections.
*
* @param array $assets
* An asset collection.
*
* @return array
* A sorted array of asset groups.
*/
public function group(array $assets);
}

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetCollectionOptimizerInterface.
*/
namespace Drupal\Core\Asset;
/**
* Interface defining a service that optimizes a collection of assets.
*/
interface AssetCollectionOptimizerInterface {
/**
* Optimizes a collection of assets.
*
* @param array $assets
* An asset collection.
*
* @return array
* An optimized asset collection.
*/
public function optimize(array $assets);
/**
* Returns all optimized asset collections assets.
*
* @return string[]
* URIs for all optimized asset collection assets.
*/
public function getAll();
/**
* Deletes all optimized asset collections assets.
*/
public function deleteAll();
}

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetCollectionRendererInterface.
*/
namespace Drupal\Core\Asset;
/**
* Interface defining a service that generates a render array to render assets.
*/
interface AssetCollectionRendererInterface {
/**
* Renders an asset collection.
*
* @param array $assets
* An asset collection.
*
* @return array
* A render array to render the asset collection.
*/
public function render(array $assets);
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetDumper.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\AssetDumperInterface;
use Drupal\Component\Utility\Crypt;
/**
* Dumps a CSS or JavaScript asset.
*/
class AssetDumper implements AssetDumperInterface {
/**
* {@inheritdoc}
*
* The file name for the CSS or JS cache file is generated from the hash of
* 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;
// Create the css/ or js/ path within the files folder.
$path = 'public://' . $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)) {
return FALSE;
}
// If CSS/JS gzip compression is enabled and the zlib extension is available
// then create a gzipped version of this file. This file is served
// conditionally to browsers that accept gzip using .htaccess rules.
// It's possible that the rewrite rules in .htaccess aren't working on this
// server, but there's no harm (other than the time spent generating the
// file) in generating the file anyway. Sites on servers where rewrite rules
// aren't working can set css.gzip to FALSE in order to skip
// generating a file that won't be used.
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get($file_extension . '.gzip')) {
if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) {
return FALSE;
}
}
return $uri;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetDumperInterface.
*/
namespace Drupal\Core\Asset;
/**
* Interface defining a service that dumps an (optimized) asset.
*/
interface AssetDumperInterface {
/**
* Dumps an (optimized) asset to persistent storage.
*
* @param string $data
* An (optimized) asset's contents.
* @param string $file_extension
* The file extension of this asset.
*
* @return string
* An URI to access the dumped asset.
*/
public function dump($data, $file_extension);
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetOptimizerInterface.
*/
namespace Drupal\Core\Asset;
/**
* Interface defining a service that optimizes an asset.
*/
interface AssetOptimizerInterface {
/**
* Optimizes an asset.
*
* @param array $asset
* An asset.
*
* @return string
* The optimized asset's contents.
*/
public function optimize(array $asset);
/**
* Removes unwanted content from an asset.
*
* @param string $content
* The content of an asset.
*
* @return string
* The cleaned asset's contents.
*/
public function clean($content);
}

View file

@ -0,0 +1,409 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetResolver.
*/
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* The default asset resolver.
*/
class AssetResolver implements AssetResolverInterface {
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* The library dependency resolver.
*
* @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
*/
protected $libraryDependencyResolver;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
protected $languageManager;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Constructs a new AssetResolver instance.
*
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
* The library discovery service.
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
* The library dependency resolver.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
*/
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
$this->libraryDiscovery = $library_discovery;
$this->libraryDependencyResolver = $library_dependency_resolver;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
$this->languageManager = $language_manager;
$this->cache = $cache;
}
/**
* Returns the libraries that need to be loaded.
*
* For example, with core/a depending on core/c and core/b on core/d:
* @code
* $assets = new AttachedAssets();
* $assets->setLibraries(['core/a', 'core/b', 'core/c']);
* $assets->setAlreadyLoadedLibraries(['core/c']);
* $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
* @endcode
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
*
* @return string[]
* A list of libraries and their dependencies, in the order they should be
* loaded, excluding any libraries that have already been loaded.
*/
protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
return array_diff(
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
);
}
/**
* {@inheritdoc}
*/
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_css_alter().
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
$css = [];
$default_options = [
'type' => 'file',
'group' => CSS_AGGREGATE_DEFAULT,
'weight' => 0,
'every_page' => FALSE,
'media' => 'all',
'preprocess' => TRUE,
'browsers' => [],
];
foreach ($this->getLibrariesToLoad($assets) as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['css'])) {
foreach ($definition['css'] as $options) {
$options += $default_options;
$options['browsers'] += [
'IE' => TRUE,
'!IE' => TRUE,
];
// Files with a query string cannot be preprocessed.
if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
$options['preprocess'] = FALSE;
}
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($css) / 1000;
// CSS files are being keyed by the full path.
$css[$options['data']] = $options;
}
}
}
// Allow modules and themes to alter the CSS assets.
$this->moduleHandler->alter('css', $css, $assets);
$this->themeManager->alter('css', $css, $assets);
// Sort CSS items, so that they appear in the correct order.
uasort($css, 'static::sort');
// Allow themes to remove CSS files by CSS files full path and file name.
if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
foreach ($css as $key => $options) {
if (isset($stylesheet_remove[$key])) {
unset($css[$key]);
}
}
}
if ($optimize) {
$css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
}
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
return $css;
}
/**
* Returns the JavaScript settings assets for this response's libraries.
*
* Gathers all drupalSettings from all libraries in the attached assets
* collection and merges them, then it merges individual attached settings,
* and finally invokes hook_js_settings_alter() to allow alterations of
* JavaScript settings by modules and themes.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* @return array
* A (possibly optimized) collection of JavaScript assets.
*/
protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
$settings = [];
foreach ($this->getLibrariesToLoad($assets) as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['drupalSettings'])) {
$settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
}
}
// Attached settings win over settings in libraries.
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
return $settings;
}
/**
* {@inheritdoc}
*/
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_js_alter(). Additionally add the current language to support
// translation of JavaScript files.
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets));
if ($cached = $this->cache->get($cid)) {
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
}
else {
$javascript = [];
$default_options = [
'type' => 'file',
'group' => JS_DEFAULT,
'every_page' => FALSE,
'weight' => 0,
'cache' => TRUE,
'preprocess' => TRUE,
'attributes' => [],
'version' => NULL,
'browsers' => [],
];
$libraries_to_load = $this->getLibrariesToLoad($assets);
// Collect all libraries that contain JS assets and are in the header.
$header_js_libraries = [];
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js']) && !empty($definition['header'])) {
$header_js_libraries[] = $library;
}
}
// The current list of header JS libraries are only those libraries that
// are in the header, but their dependencies must also be loaded for them
// to function correctly, so update the list with those.
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js'])) {
foreach ($definition['js'] as $options) {
$options += $default_options;
// 'scope' is a calculated option, based on which libraries are
// marked to be loaded from the header (see above).
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
// Preprocess can only be set if caching is enabled and no
// attributes are set.
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($javascript) / 1000;
// Local and external files must keep their name as the associative
// key so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options;
}
}
}
// Allow modules and themes to alter the JavaScript assets.
$this->moduleHandler->alter('js', $javascript, $assets);
$this->themeManager->alter('js', $javascript, $assets);
// Sort JavaScript assets, so that they appear in the correct order.
uasort($javascript, 'static::sort');
// Prepare the return value: filter JavaScript assets per scope.
$js_assets_header = [];
$js_assets_footer = [];
foreach ($javascript as $key => $item) {
if ($item['scope'] == 'header') {
$js_assets_header[$key] = $item;
}
elseif ($item['scope'] == 'footer') {
$js_assets_footer[$key] = $item;
}
}
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);
}
// If the core/drupalSettings library is being loaded or is already
// loaded, get the JavaScript settings assets, and convert them into a
// single "regular" JavaScript asset.
$libraries_to_load = $this->getLibrariesToLoad($assets);
$settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
// Initialize settings to FALSE since they are not needed by default. This
// distinguishes between an empty array which must still allow
// hook_js_settings_alter() to be run.
$settings = FALSE;
if ($settings_required && $settings_have_changed) {
$settings = $this->getJsSettingsAssets($assets);
// Allow modules to add cached JavaScript settings.
foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
$function = $module . '_' . 'js_settings_build';
$function($settings, $assets);
}
}
$settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
}
if ($settings !== FALSE) {
// Allow modules and themes to alter the JavaScript settings.
$this->moduleHandler->alter('js_settings', $settings, $assets);
$this->themeManager->alter('js_settings', $settings, $assets);
$settings_as_inline_javascript = [
'type' => 'setting',
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => [],
'data' => $settings,
];
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
// Prepend to the list of JS assets, to render it first. Preferably in
// the footer, but in the header if necessary.
if ($settings_in_header) {
$js_assets_header = $settings_js_asset + $js_assets_header;
}
else {
$js_assets_footer = $settings_js_asset + $js_assets_footer;
}
}
return [
$js_assets_header,
$js_assets_footer,
];
}
/**
* Sorts CSS and JavaScript resources.
*
* This sort order helps optimize front-end performance while providing
* modules and themes with the necessary control for ordering the CSS and
* JavaScript appearing on a page.
*
* @param $a
* First item for comparison. The compared items should be associative
* arrays of member items.
* @param $b
* Second item for comparison.
*
* @return int
*/
public static function sort($a, $b) {
// First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
// group appear before items in the CSS_AGGREGATE_THEME group. Modules may
// create additional groups by defining their own constants.
if ($a['group'] < $b['group']) {
return -1;
}
elseif ($a['group'] > $b['group']) {
return 1;
}
// Within a group, order all infrequently needed, page-specific files after
// common files needed throughout the website. Separating this way allows
// for the aggregate file generated for all of the common files to be reused
// across a site visit without being cut by a page using a less common file.
elseif ($a['every_page'] && !$b['every_page']) {
return -1;
}
elseif (!$a['every_page'] && $b['every_page']) {
return 1;
}
// Finally, order by weight.
elseif ($a['weight'] < $b['weight']) {
return -1;
}
elseif ($a['weight'] > $b['weight']) {
return 1;
}
else {
return 0;
}
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AssetResolverInterface.
*/
namespace Drupal\Core\Asset;
/**
* Resolves asset libraries into concrete CSS and JavaScript assets.
*
* Given an attached assets collection (to be loaded for the current response),
* the asset resolver can resolve those asset libraries into a list of concrete
* CSS and JavaScript assets.
*
* In other words: this allows developers to translate Drupal's asset
* abstraction (asset libraries) into concrete assets.
*
* @see \Drupal\Core\Asset\AttachedAssetsInterface
* @see \Drupal\Core\Asset\LibraryDependencyResolverInterface
*/
interface AssetResolverInterface {
/**
* Returns the CSS assets for the current response's libraries.
*
* It returns the CSS assets in order, according to the SMACSS categories
* specified in the assets' weights:
* - CSS_BASE
* - CSS_LAYOUT
* - CSS_COMPONENT
* - CSS_STATE
* - CSS_THEME
* @see https://www.drupal.org/node/1887918#separate-concerns
* This ensures proper cascading of styles so themes can easily override
* module styles through CSS selectors.
*
* Themes may replace module-defined CSS files by adding a stylesheet with the
* same filename. For example, themes/bartik/system-menus.css would replace
* modules/system/system-menus.css. This allows themes to override complete
* CSS files, rather than specific selectors, when necessary.
*
* Also invokes hook_css_alter(), to allow CSS assets to be altered.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* @param bool $optimize
* Whether to apply the CSS asset collection optimizer, to return an
* optimized CSS asset collection rather than an unoptimized one.
*
* @return array
* A (possibly optimized) collection of CSS assets.
*/
public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
/**
* Returns the JavaScript assets for the current response's libraries.
*
* References to JavaScript files are placed in a certain order: first, all
* 'core' files, then all 'module' and finally all 'theme' JavaScript files
* are added to the page. Then, all settings are output, followed by 'inline'
* JavaScript code. If running update.php, all preprocessing is disabled.
*
* Note that hook_js_alter(&$javascript) is called during this function call
* to allow alterations of the JavaScript during its presentation. The correct
* way to add JavaScript during hook_js_alter() is to add another element to
* the $javascript array, deriving from drupal_js_defaults(). See
* locale_js_alter() for an example of this.
*
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
* @param bool $optimize
* Whether to apply the JavaScript asset collection optimizer, to return
* optimized JavaScript asset collections rather than an unoptimized ones.
*
* @return array
* A nested array containing 2 values:
* - at index zero: the (possibly optimized) collection of JavaScript assets
* for the top of the page
* - at index one: the (possibly optimized) collection of JavaScript assets
* for the bottom of the page
*/
public function getJsAssets(AttachedAssetsInterface $assets, $optimize);
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AttachedAssets.
*/
namespace Drupal\Core\Asset;
/**
* The default attached assets collection.
*/
class AttachedAssets implements AttachedAssetsInterface {
/**
* The (ordered) list of asset libraries attached to the current response.
*
* @var string[]
*/
public $libraries = [];
/**
* The JavaScript settings attached to the current response.
*
* @var array
*/
public $settings = [];
/**
* The set of asset libraries that the client has already loaded.
*
* @var string[]
*/
protected $alreadyLoadedLibraries = [];
/**
* {@inheritdoc}
*/
public static function createFromRenderArray(array $render_array) {
if (!isset($render_array['#attached'])) {
throw new \LogicException('The render array has not yet been rendered, hence not all attachments have been collected yet.');
}
$assets = new static();
if (isset($render_array['#attached']['library'])) {
$assets->setLibraries($render_array['#attached']['library']);
}
if (isset($render_array['#attached']['drupalSettings'])) {
$assets->setSettings($render_array['#attached']['drupalSettings']);
}
return $assets;
}
/**
* {@inheritdoc}
*/
public function setLibraries(array $libraries) {
$this->libraries = array_unique($libraries);
return $this;
}
/**
* {@inheritdoc}
*/
public function getLibraries() {
return $this->libraries;
}
/**
* {@inheritdoc}
*/
public function setSettings(array $settings) {
$this->settings = $settings;
return $this;
}
/**
* {@inheritdoc}
*/
public function getSettings() {
return $this->settings;
}
/**
* {@inheritdoc}
*/
public function getAlreadyLoadedLibraries() {
return $this->alreadyLoadedLibraries;
}
/**
* {@inheritdoc}
*/
public function setAlreadyLoadedLibraries(array $libraries) {
$this->alreadyLoadedLibraries = $libraries;
return $this;
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\AttachedAssetsInterface.
*/
namespace Drupal\Core\Asset;
/**
* The attached assets collection for the current response.
*
* Allows for storage of:
* - an ordered list of asset libraries (to be loaded for the current response)
* - attached JavaScript settings (to be loaded for the current response)
* - a set of asset libraries that the client already has loaded (as indicated
* in the request, to *not* be loaded for the current response)
*
* @see \Drupal\Core\Asset\AssetResolverInterface
*/
interface AttachedAssetsInterface {
/**
* Creates an AttachedAssetsInterface object from a render array.
*
* @param array $render_array
* A render array.
*
* @return \Drupal\Core\Asset\AttachedAssetsInterface
*
* @throws \LogicException
*/
public static function createFromRenderArray(array $render_array);
/**
* Sets the asset libraries attached to the current response.
*
* @param string[] $libraries
* A list of libraries, in the order they should be loaded.
*
* @return $this
*/
public function setLibraries(array $libraries);
/**
* Returns the asset libraries attached to the current response.
*
* @return string[]
*/
public function getLibraries();
/**
* Sets the JavaScript settings that are attached to the current response.
*
* @param array $settings
* The needed JavaScript settings.
*
* @return $this
*/
public function setSettings(array $settings);
/**
* Returns the settings attached to the current response.
*
* @return array
*/
public function getSettings();
/**
* Sets the asset libraries that the current request marked as already loaded.
*
* @param string[] $libraries
* The set of already loaded libraries.
*
* @return $this
*/
public function setAlreadyLoadedLibraries(array $libraries);
/**
* Returns the set of already loaded asset libraries.
*
* @return string[]
*/
public function getAlreadyLoadedLibraries();
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\CssCollectionGrouper.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
/**
* Groups CSS assets.
*/
class CssCollectionGrouper implements AssetCollectionGrouperInterface {
/**
* {@inheritdoc}
*
* Puts multiple items into the same group if they are groupable and if they
* are for the same 'media' and 'browsers'. Items of the 'file' type are
* groupable if their 'preprocess' flag is TRUE, items of the 'inline' type
* are always groupable, and items of the 'external' type are never groupable.
*
* Also ensures that the process of grouping items does not change their
* relative order. This requirement may result in multiple groups for the same
* type, media, and browsers, if needed to accommodate other items in between.
*/
public function group(array $css_assets) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
// When creating a new group, we pre-increment $i, so by initializing it to
// -1, the first group will have index 0.
$i = -1;
foreach ($css_assets as $item) {
// The browsers for which the CSS item needs to be loaded is part of the
// information that determines when a new group is needed, but the order
// of keys in the array doesn't matter, and we don't want a new group if
// all that's different is that order.
ksort($item['browsers']);
// If the item can be grouped with other items, set $group_keys to an
// array of information that must be the same for all items in its group.
// If the item can't be grouped with other items, set $group_keys to
// FALSE. We put items into a group that can be aggregated together:
// whether they will be aggregated is up to the _drupal_css_aggregate()
// function or an
// override of that function specified in hook_css_alter(), but regardless
// of the details of that function, a group represents items that can be
// aggregated. Since a group may be rendered with a single HTML tag, all
// items in the group must share the same information that would need to
// be part of that HTML tag.
switch ($item['type']) {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE;
break;
case 'inline':
// Always group inline items.
$group_keys = array($item['type'], $item['media'], $item['browsers']);
break;
case 'external':
// Do not group external items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$i++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data', 'weight' and 'basename'
// properties are unique to the item and should not be carried over to
// the group.
$groups[$i] = $item;
unset($groups[$i]['data'], $groups[$i]['weight'], $groups[$i]['basename']);
$groups[$i]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$i]['items'][] = $item;
}
return $groups;
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\CssCollectionOptimizer.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\State\StateInterface;
/**
* Optimizes CSS assets.
*/
class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
/**
* A CSS asset grouper.
*
* @var \Drupal\Core\Asset\CssCollectionGrouper
*/
protected $grouper;
/**
* A CSS asset optimizer.
*
* @var \Drupal\Core\Asset\CssOptimizer
*/
protected $optimizer;
/**
* An asset dumper.
*
* @var \Drupal\Core\Asset\AssetDumper
*/
protected $dumper;
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a CssCollectionOptimizer.
*
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface
* 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) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
}
/**
* {@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 file names
* in $css 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 $css 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.
*/
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();
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;
unset($css_assets[$order]['items']);
switch ($css_group['type']) {
case '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;
}
// 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 proceed 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;
case 'inline':
// We don't do any caching for inline CSS assets.
$data = '';
foreach ($css_group['items'] as $css_asset) {
$data .= $this->optimizer->optimize($css_asset);
}
unset($css_assets[$order]['data']['items']);
$css_assets[$order]['data'] = $data;
break;
case '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;
break;
}
}
return $css_assets;
}
/**
* 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) {
$css_data = array();
foreach ($css_group['items'] as $css_file) {
$css_data[] = $css_file['data'];
}
return hash('sha256', serialize($css_data));
}
/**
* {@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) {
// Default stale file threshold is 30 days.
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
file_unmanaged_delete($uri);
}
};
file_scan_directory('public://css', '/.*/', array('callback' => $delete_stale));
}
}

View file

@ -0,0 +1,223 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\CssCollectionRenderer.
*/
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\State\StateInterface;
/**
* Renders CSS assets.
*
* For production websites, LINK tags are preferable to STYLE tags with @import
* statements, because:
* - They are the standard tag intended for linking to a resource.
* - On Firefox 2 and perhaps other browsers, CSS files included with @import
* statements don't get saved when saving the complete web page for offline
* use: https://www.drupal.org/node/145218.
* - On IE, if only LINK tags and no @import statements are used, all the CSS
* files are downloaded in parallel, resulting in faster page load, but if
* @import statements are used and span across multiple STYLE tags, all the
* ones from one STYLE tag must be downloaded before downloading begins for
* the next STYLE tag. Furthermore, IE7 does not support media declaration on
* the @import statement, so multiple STYLE tags must be used when different
* files are for different media types. Non-IE browsers always download in
* parallel, so this is an IE-specific performance quirk:
* http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
*
* However, IE has an annoying limit of 31 total CSS inclusion tags
* (https://www.drupal.org/node/228818) and LINK tags are limited to one file
* per tag, whereas STYLE tags can contain multiple @import statements allowing
* multiple files to be loaded per tag. When CSS aggregation is disabled, a
* Drupal site can easily have more than 31 CSS files that need to be loaded, so
* using LINK tags exclusively would result in a site that would display
* incorrectly in IE. Depending on different needs, different strategies can be
* employed to decide when to use LINK tags and when to use STYLE tags.
*
* The strategy employed by this class is to use LINK tags for all aggregate
* files and for all files that cannot be aggregated (e.g., if 'preprocess' is
* set to FALSE or the type is 'external'), and to use STYLE tags for groups
* of files that could be aggregated together but aren't (e.g., if the site-wide
* aggregation setting is disabled). This results in all LINK tags when
* aggregation is enabled, a guarantee that as many or only slightly more tags
* are used with aggregation disabled than enabled (so that if the limit were to
* be crossed with aggregation enabled, the site developer would also notice the
* problem while aggregation is disabled), and an easy way for a developer to
* view HTML source while aggregation is disabled and know what files will be
* aggregated together when aggregation becomes enabled.
*
* This class evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this class to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*/
class CssCollectionRenderer implements AssetCollectionRendererInterface {
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a CssCollectionRenderer.
*
* @param \Drupal\Core\State\StateInterface
* The state key/value store.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function render(array $css_assets) {
$elements = array();
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed.
$query_string = $this->state->get('system.css_js_query_string') ?: '0';
// Defaults for LINK and STYLE elements.
$link_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'link',
'#attributes' => array(
'rel' => 'stylesheet',
),
);
$style_element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'style',
);
// For filthy IE hack.
$current_ie_group_keys = NULL;
$get_ie_group_key = function ($css_asset) {
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['every_page'], $css_asset['media'], $css_asset['browsers']);
};
// Loop through all CSS assets, by key, to allow for the special IE
// workaround.
$css_assets_keys = array_keys($css_assets);
for ($i = 0; $i < count($css_assets_keys); $i++) {
$css_asset = $css_assets[$css_assets_keys[$i]];
switch ($css_asset['type']) {
// For file items, there are three possibilities.
// - There are up to 31 CSS assets on the page (some of which may be
// aggregated). In this case, output a LINK tag for file CSS assets.
// - There are more than 31 CSS assets on the page, yet we must stay
// below IE<10's limit of 31 total CSS inclusion tags, we handle this
// in two ways:
// - file CSS assets that are not eligible for aggregation (their
// 'preprocess' flag has been set to FALSE): in this case, output a
// LINK tag.
// - file CSS assets that can be aggregated (and possibly have been):
// in this case, figure out which subsequent file CSS assets share
// the same key properties ('group', 'every_page', 'media' and
// 'browsers') and output this group into as few STYLE tags as
// possible (a STYLE tag may contain only 31 @import statements).
case 'file':
// The dummy query string needs to be added to the URL to control
// browser-caching.
$query_string_separator = (strpos($css_asset['data'], '?') !== FALSE) ? '&' : '?';
// As long as the current page will not run into IE's limit for CSS
// assets: output a LINK tag for a file CSS asset.
if (count($css_assets) <= 31) {
$element = $link_element_defaults;
$element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string;
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
// The current page will run into IE's limits for CSS assets: work
// around these limits by performing a light form of grouping.
// Once Drupal only needs to support IE10 and later, we can drop this.
else {
// The file CSS asset is ineligible for aggregation: output it in a
// LINK tag.
if (!$css_asset['preprocess']) {
$element = $link_element_defaults;
$element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string;
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
// The file CSS asset can be aggregated, but hasn't been: combine
// multiple items into as few STYLE tags as possible.
else {
$import = array();
// Start with the current CSS asset, iterate over subsequent CSS
// assets and find which ones have the same 'type', 'group',
// 'every_page', 'preprocess', 'media' and 'browsers' properties.
$j = $i;
$next_css_asset = $css_asset;
$current_ie_group_key = $get_ie_group_key($css_asset);
do {
// The dummy query string needs to be added to the URL to
// control browser-caching. IE7 does not support a media type on
// the @import statement, so we instead specify the media for
// the group on the STYLE tag.
$import[] = '@import url("' . SafeMarkup::checkPlain(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
// Move the outer for loop skip the next item, since we
// processed it here.
$i = $j;
// Retrieve next CSS asset, unless there is none: then break.
if ($j + 1 < count($css_assets_keys)) {
$j++;
$next_css_asset = $css_assets[$css_assets_keys[$j]];
}
else {
break;
}
} while ($get_ie_group_key($next_css_asset) == $current_ie_group_key);
// In addition to IE's limit of 31 total CSS inclusion tags, it
// also has a limit of 31 @import statements per STYLE tag.
while (!empty($import)) {
$import_batch = array_slice($import, 0, 31);
$import = array_slice($import, 31);
$element = $style_element_defaults;
// This simplifies the JavaScript regex, allowing each line
// (separated by \n) to be treated as a completely different
// string. This means that we can use ^ and $ on one line at a
// time, and not worry about style tags since they'll never
// match the regex.
$element['#value'] = "\n" . implode("\n", $import_batch) . "\n";
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
}
}
}
break;
// Output a LINK tag for an external CSS asset. The asset's 'data'
// property contains the full URL.
case 'external':
$element = $link_element_defaults;
$element['#attributes']['href'] = $css_asset['data'];
$element['#attributes']['media'] = $css_asset['media'];
$element['#browsers'] = $css_asset['browsers'];
$elements[] = $element;
break;
default:
throw new \Exception('Invalid CSS asset type.');
}
}
return $elements;
}
}

View file

@ -0,0 +1,272 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\CssOptimizer.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\AssetOptimizerInterface;
use Drupal\Component\Utility\Unicode;
/**
* Optimizes a CSS asset.
*/
class CssOptimizer implements AssetOptimizerInterface {
/**
* The base path used by rewriteFileURI().
*
* @var string
*/
public $rewriteFileURIBasePath;
/**
* {@inheritdoc}
*/
public function optimize(array $css_asset) {
if (!in_array($css_asset['type'], array('file', 'inline'))) {
throw new \Exception('Only file or inline CSS assets can be optimized.');
}
if ($css_asset['type'] === 'file' && !$css_asset['preprocess']) {
throw new \Exception('Only file CSS assets with preprocessing enabled can be optimized.');
}
if ($css_asset['type'] === 'file') {
return $this->processFile($css_asset);
}
else {
return $this->processCss($css_asset['data'], $css_asset['preprocess']);
}
}
/**
* Processes the contents of a CSS asset for cleanup.
*
* @param string $contents
* The contents of the CSS asset.
*
* @return string
* Contents of the CSS asset.
*/
public function clean($contents) {
// Remove multiple charset declarations for standards compliance (and fixing
// Safari problems).
$contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
return $contents;
}
/**
* Build aggregate CSS file.
*/
protected function processFile($css_asset) {
$contents = $this->loadFile($css_asset['data'], TRUE);
$contents = $this->clean($contents);
// Get the parent directory of this file, relative to the Drupal root.
$css_base_path = substr($css_asset['data'], 0, strrpos($css_asset['data'], '/'));
// Store base path.
$this->rewriteFileURIBasePath = $css_base_path . '/';
// Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
return preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', array($this, 'rewriteFileURI'), $contents);
}
/**
* Loads the stylesheet and resolves all @import commands.
*
* Loads a stylesheet and replaces @import commands with the contents of the
* imported file. Use this instead of file_get_contents when processing
* stylesheets.
*
* The returned contents are compressed removing white space and comments only
* when CSS aggregation is enabled. This optimization will not apply for
* color.module enabled themes with CSS aggregation turned off.
*
* Note: the only reason this method is public is so color.module can call it;
* it is not on the AssetOptimizerInterface, so future refactorings can make
* it protected.
*
* @param $file
* Name of the stylesheet to be processed.
* @param $optimize
* Defines if CSS contents should be compressed or not.
* @param $reset_basepath
* Used internally to facilitate recursive resolution of @import commands.
*
* @return
* Contents of the stylesheet, including any resolved @import commands.
*/
public function loadFile($file, $optimize = NULL, $reset_basepath = TRUE) {
// These statics are not cache variables, so we don't use drupal_static().
static $_optimize, $basepath;
if ($reset_basepath) {
$basepath = '';
}
// Store the value of $optimize for preg_replace_callback with nested
// @import loops.
if (isset($optimize)) {
$_optimize = $optimize;
}
// Stylesheets are relative one to each other. Start by adding a base path
// prefix provided by the parent stylesheet (if necessary).
if ($basepath && !file_uri_scheme($file)) {
$file = $basepath . '/' . $file;
}
// Store the parent base path to restore it later.
$parent_base_path = $basepath;
// Set the current base path to process possible child imports.
$basepath = dirname($file);
// Load the CSS stylesheet. We suppress errors because themes may specify
// stylesheets in their .info.yml file that don't exist in the theme's path,
// but are merely there to disable certain module CSS files.
$content = '';
if ($contents = @file_get_contents($file)) {
// If a BOM is found, convert the file to UTF-8, then use substr() to
// remove the BOM from the result.
if ($encoding = (Unicode::encodingFromBOM($contents))) {
$contents = Unicode::substr(Unicode::convertToUtf8($contents, $encoding), 1);
}
// If no BOM, check for fallback encoding. Per CSS spec the regex is very strict.
elseif (preg_match('/^@charset "([^"]+)";/', $contents, $matches)) {
if ($matches[1] !== 'utf-8' && $matches[1] !== 'UTF-8') {
$contents = substr($contents, strlen($matches[0]));
$contents = Unicode::convertToUtf8($contents, $matches[1]);
}
}
// Return the processed stylesheet.
$content = $this->processCss($contents, $_optimize);
}
// Restore the parent base path as the file and its children are processed.
$basepath = $parent_base_path;
return $content;
}
/**
* Loads stylesheets recursively and returns contents with corrected paths.
*
* This function is used for recursive loading of stylesheets and
* returns the stylesheet content with all url() paths corrected.
*
* @param array $matches
* An array of matches by a preg_replace_callback() call that scans for
* @import-ed CSS files, except for external CSS files.
*
* @return
* The contents of the CSS file at $matches[1], with corrected paths.
*
* @see \Drupal\Core\Asset\AssetOptimizerInterface::loadFile()
*/
protected function loadNestedFile($matches) {
$filename = $matches[1];
// Load the imported stylesheet and replace @import commands in there as
// well.
$file = $this->loadFile($filename, NULL, FALSE);
// Determine the file's directory.
$directory = dirname($filename);
// If the file is in the current directory, make sure '.' doesn't appear in
// the url() path.
$directory = $directory == '.' ? '' : $directory .'/';
// Alter all internal url() paths. Leave external paths alone. We don't need
// to normalize absolute paths here because that will be done later.
return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file);
}
/**
* Processes the contents of a stylesheet for aggregation.
*
* @param $contents
* The contents of the stylesheet.
* @param $optimize
* (optional) Boolean whether CSS contents should be minified. Defaults to
* FALSE.
*
* @return
* Contents of the stylesheet including the imported stylesheets.
*/
protected function processCss($contents, $optimize = FALSE) {
// Remove unwanted CSS code that cause issues.
$contents = $this->clean($contents);
if ($optimize) {
// Perform some safe CSS optimizations.
// Regexp to match comment blocks.
$comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
// Regexp to match double quoted strings.
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
// Regexp to match single quoted strings.
$single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
// Strip all comment blocks, but keep double/single quoted strings.
$contents = preg_replace(
"<($double_quot|$single_quot)|$comment>Ss",
"$1",
$contents
);
// Remove certain whitespace.
// There are different conditions for removing leading and trailing
// whitespace.
// @see http://php.net/manual/regexp.reference.subpatterns.php
$contents = preg_replace('<
# Strip leading and trailing whitespace.
\s*([@{};,])\s*
# Strip only leading whitespace from:
# - Closing parenthesis: Retain "@media (bar) and foo".
| \s+([\)])
# Strip only trailing whitespace from:
# - Opening parenthesis: Retain "@media (bar) and foo".
# - Colon: Retain :pseudo-selectors.
| ([\(:])\s+
>xS',
// Only one of the three capturing groups will match, so its reference
// will contain the wanted value and the references for the
// two non-matching groups will be replaced with empty strings.
'$1$2$3',
$contents
);
// End the file with a new line.
$contents = trim($contents);
$contents .= "\n";
}
// Replaces @import commands with the actual stylesheet content.
// This happens recursively but omits external files.
$contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', array($this, 'loadNestedFile'), $contents);
return $contents;
}
/**
* Prefixes all paths within a CSS file for processFile().
*
* @param array $matches
* An array of matches by a preg_replace_callback() call that scans for
* url() references in CSS files, except for external or absolute ones.
*
* Note: the only reason this method is public is so color.module can call it;
* it is not on the AssetOptimizerInterface, so future refactorings can make
* it protected.
*
* @return string
* The file path.
*/
public function rewriteFileURI($matches) {
// Prefix with base and remove '../' segments where possible.
$path = $this->rewriteFileURIBasePath . $matches[1];
$last = '';
while ($path != $last) {
$last = $path;
$path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
}
return 'url(' . file_create_url($path) . ')';
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException.
*/
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a library has no CSS/JS/JS setting specified.
*/
class IncompleteLibraryDefinitionException extends \RuntimeException {
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\Exception\InvalidLibraryFileException.
*/
namespace Drupal\Core\Asset\Exception;
/**
* Defines an exception if the library file could not be parsed.
*/
class InvalidLibraryFileException extends \RunTimeException {
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException.
*/
namespace Drupal\Core\Asset\Exception;
/**
* Defines a custom exception if a library has a remote but no license.
*/
class LibraryDefinitionMissingLicenseException extends \RuntimeException {
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\JsCollectionGrouper.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
/**
* Groups JavaScript assets.
*/
class JsCollectionGrouper implements AssetCollectionGrouperInterface {
/**
* {@inheritdoc}
*
* Puts multiple items into the same group if they are groupable and if they
* are for the same browsers. Items of the 'file' type are groupable if their
* 'preprocess' flag is TRUE. Items of the 'inline', 'settings', or 'external'
* type are not groupable.
*
* Also ensures that the process of grouping items does not change their
* relative order. This requirement may result in multiple groups for the same
* type and browsers, if needed to accommodate other items in between.
*/
public function group(array $js_assets) {
$groups = array();
// If a group can contain multiple items, we track the information that must
// be the same for each item in the group, so that when we iterate the next
// item, we can determine if it can be put into the current group, or if a
// new group needs to be made for it.
$current_group_keys = NULL;
$index = -1;
foreach ($js_assets as $item) {
// The browsers for which the JavaScript item needs to be loaded is part
// of the information that determines when a new group is needed, but the
// order of keys in the array doesn't matter, and we don't want a new
// group if all that's different is that order.
ksort($item['browsers']);
switch ($item['type']) {
case 'file':
// Group file items if their 'preprocess' flag is TRUE.
// Help ensure maximum reuse of aggregate files by only grouping
// together items that share the same 'group' value and 'every_page'
// flag.
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['browsers']) : FALSE;
break;
case 'external':
case 'setting':
case 'inline':
// Do not group external, settings, and inline items.
$group_keys = FALSE;
break;
}
// If the group keys don't match the most recent group we're working with,
// then a new group must be made.
if ($group_keys !== $current_group_keys) {
$index++;
// Initialize the new group with the same properties as the first item
// being placed into it. The item's 'data' and 'weight' properties are
// unique to the item and should not be carried over to the group.
$groups[$index] = $item;
unset($groups[$index]['data'], $groups[$index]['weight']);
$groups[$index]['items'] = array();
$current_group_keys = $group_keys ? $group_keys : NULL;
}
// Add the item to the current group.
$groups[$index]['items'][] = $item;
}
return $groups;
}
}

View file

@ -0,0 +1,197 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\JsCollectionOptimizer.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\State\StateInterface;
/**
* Optimizes JavaScript assets.
*/
class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
/**
* A JS asset grouper.
*
* @var \Drupal\Core\Asset\JsCollectionGrouper
*/
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.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* 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.
*/
public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) {
$this->grouper = $grouper;
$this->optimizer = $optimizer;
$this->dumper = $dumper;
$this->state = $state;
}
/**
* {@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.
*/
public function optimize(array $js_assets) {
// Group the assets.
$js_groups = $this->grouper->group($js_assets);
// Now optimize (concatenate, not minify) and dump each asset group, unless
// that was already done, in which case it should appear in
// system.js_cache_files.
// 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
// 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;
unset($js_assets[$order]['items']);
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;
}
// 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;
}
$js_assets[$order]['preprocessed'] = TRUE;
}
break;
case 'external':
case 'setting':
case 'inline':
// We don't do any aggregation and hence also no caching for external,
// setting or inline JS assets.
$uri = $js_group['items'][0]['data'];
$js_assets[$order]['data'] = $uri;
break;
}
}
return $js_assets;
}
/**
* Generate a hash for a given group of JavaScript assets.
*
* @param array $js_group
* A group of JavaScript assets.
*
* @return string
* A hash to uniquely identify the given group of JavaScript assets.
*/
protected function generateHash(array $js_group) {
$js_data = array();
foreach ($js_group['items'] as $js_file) {
$js_data[] = $js_file['data'];
}
return hash('sha256', serialize($js_data));
}
/**
* {@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) {
// Default stale file threshold is 30 days.
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
file_unmanaged_delete($uri);
}
};
file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale));
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\JsCollectionRenderer.
*/
namespace Drupal\Core\Asset;
use Drupal\Component\Serialization\Json;
use Drupal\Core\State\StateInterface;
/**
* Renders JavaScript assets.
*/
class JsCollectionRenderer implements AssetCollectionRendererInterface {
/**
* The state key/value store.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a JsCollectionRenderer.
*
* @param \Drupal\Core\State\StateInterface
* The state key/value store.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*
* This class evaluates the aggregation enabled/disabled condition on a group
* by group basis by testing whether an aggregate file has been made for the
* group rather than by testing the site-wide aggregation setting. This allows
* this class to work correctly even if modules have implemented custom
* logic for grouping and aggregating files.
*/
public function render(array $js_assets) {
$elements = array();
// A dummy query-string is added to filenames, to gain control over
// browser-caching. The string changes on every update or full cache
// flush, forcing browsers to load a new copy of the files, as the
// URL changed. Files that should not be cached get REQUEST_TIME as
// query-string instead, to enforce reload on every page request.
$default_query_string = $this->state->get('system.css_js_query_string') ?: '0';
// For inline JavaScript to validate as XHTML, all JavaScript containing
// XHTML needs to be wrapped in CDATA. To make that backwards compatible
// with HTML 4, we need to comment out the CDATA-tag.
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
$embed_suffix = "\n//--><!]]>\n";
// Defaults for each SCRIPT element.
$element_defaults = array(
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => '',
);
// Loop through all JS assets.
foreach ($js_assets as $js_asset) {
// Element properties that do not depend on JS asset type.
$element = $element_defaults;
$element['#browsers'] = $js_asset['browsers'];
// Element properties that depend on item type.
switch ($js_asset['type']) {
case 'setting':
$element['#value_prefix'] = $embed_prefix;
$element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
$element['#value_suffix'] = $embed_suffix;
break;
case 'file':
$query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
$query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
$element['#attributes']['src'] = file_create_url($js_asset['data']);
// Only add the cache-busting query string if this isn't an aggregate
// file.
if (!isset($js_asset['preprocessed'])) {
$element['#attributes']['src'] .= $query_string_separator . ($js_asset['cache'] ? $query_string : REQUEST_TIME);
}
break;
case 'external':
$element['#attributes']['src'] = $js_asset['data'];
break;
default:
throw new \Exception('Invalid JS asset type.');
}
// Attributes may only be set if this script is output independently.
if (!empty($element['#attributes']['src']) && !empty($js_asset['attributes'])) {
$element['#attributes'] += $js_asset['attributes'];
}
$elements[] = $element;
}
return $elements;
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\JsOptimizer.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\AssetOptimizerInterface;
use Drupal\Component\Utility\Unicode;
/**
* Optimizes a JavaScript asset.
*/
class JsOptimizer implements AssetOptimizerInterface {
/**
* {@inheritdoc}
*/
public function optimize(array $js_asset) {
if ($js_asset['type'] !== 'file') {
throw new \Exception('Only file JavaScript assets can be optimized.');
}
if ($js_asset['type'] === 'file' && !$js_asset['preprocess']) {
throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.');
}
// If a BOM is found, convert the file to UTF-8, then use substr() to
// remove the BOM from the result.
$data = file_get_contents($js_asset['data']);
if ($encoding = (Unicode::encodingFromBOM($data))) {
$data = Unicode::substr(Unicode::convertToUtf8($data, $encoding), 1);
}
// If no BOM is found, check for the charset attribute.
elseif (isset($js_asset['attributes']['charset'])) {
$data = Unicode::convertToUtf8($data, $js_asset['attributes']['charset']);
}
// No-op optimizer: no optimizations are applied to JavaScript assets.
return $data;
}
/**
* Processes the contents of a javascript asset for cleanup.
*
* @param string $contents
* The contents of the javascript asset.
*
* @return string
* Contents of the javascript asset.
*/
public function clean($contents) {
// Remove JS source and source mapping urls or these may cause 404 errors.
$contents = preg_replace('/\/\/(#|@)\s(sourceURL|sourceMappingURL)=\s*(\S*?)\s*$/m', '', $contents);
return $contents;
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDependencyResolver.
*/
namespace Drupal\Core\Asset;
/**
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
*/
class LibraryDependencyResolver implements LibraryDependencyResolverInterface {
/**
* The library discovery service.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
*/
protected $libraryDiscovery;
/**
* Constructs a new LibraryDependencyResolver instance.
*
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
* The library discovery service.
*/
public function __construct(LibraryDiscoveryInterface $library_discovery) {
$this->libraryDiscovery = $library_discovery;
}
/**
* {@inheritdoc}
*/
public function getLibrariesWithDependencies(array $libraries) {
return $this->doGetDependencies($libraries);
}
/**
* Gets the given libraries with its dependencies.
*
* Helper method for ::getLibrariesWithDependencies().
*
* @param string[] $libraries_with_unresolved_dependencies
* A list of libraries, with unresolved dependencies, in the order they
* should be loaded.
* @param string[] $final_libraries
* The final list of libraries (the return value) that is being built
* recursively.
*
* @return string[]
* A list of libraries, in the order they should be loaded, including their
* dependencies.
*/
protected function doGetDependencies(array $libraries_with_unresolved_dependencies, array $final_libraries = []) {
foreach ($libraries_with_unresolved_dependencies as $library) {
if (!in_array($library, $final_libraries)) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (!empty($definition['dependencies'])) {
$final_libraries = $this->doGetDependencies($definition['dependencies'], $final_libraries);
}
$final_libraries[] = $library;
}
}
return $final_libraries;
}
/**
* {@inheritdoc}
*/
public function getMinimalRepresentativeSubset(array $libraries) {
$minimal = [];
// Determine each library's dependencies.
$with_deps = [];
foreach ($libraries as $library) {
$with_deps[$library] = $this->getLibrariesWithDependencies([$library]);
}
foreach ($libraries as $library) {
$exists = FALSE;
foreach ($with_deps as $other_library => $dependencies) {
if ($library == $other_library) {
continue;
}
if (in_array($library, $dependencies)) {
$exists = TRUE;
break;
}
}
if (!$exists) {
$minimal[] = $library;
}
}
return $minimal;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDependencyResolverInterface.
*/
namespace Drupal\Core\Asset;
/**
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
*/
interface LibraryDependencyResolverInterface {
/**
* Gets the given libraries with their dependencies.
*
* Given ['core/a', 'core/b', 'core/c'], with core/a depending on core/c and
* core/b on core/d, returns ['core/a', 'core/b', 'core/c', 'core/d'].
*
* @param string[] $libraries
* A list of libraries, in the order they should be loaded.
*
* @return string[]
* A list of libraries, in the order they should be loaded, including their
* dependencies.
*/
public function getLibrariesWithDependencies(array $libraries);
/**
* Gets the minimal representative subset of the given libraries.
*
* A minimal representative subset means that any library in the given set of
* libraries that is a dependency of another library in the set, is removed.
*
* Hence a minimal representative subset is the most compact representation
* possible of a set of libraries.
*
* (Each asset library has dependencies and can therefore be seen as a tree.
* Hence the given list of libraries represent a forest. This function returns
* all roots of trees that are not a subtree of another tree in the forest.)
*
* @param string[] $libraries
* A set of libraries.
*
* @return string[]
* A representative subset of the given set of libraries.
*/
public function getMinimalRepresentativeSubset(array $libraries);
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDiscovery.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Cache\CacheCollectorInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* Discovers available asset libraries in Drupal.
*/
class LibraryDiscovery implements LibraryDiscoveryInterface {
/**
* The library discovery cache collector.
*
* @var \Drupal\Core\Cache\CacheCollectorInterface
*/
protected $collector;
/**
* The cache tag invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagInvalidator;
/**
* The final library definitions, statically cached.
*
* hook_library_info_alter() and hook_js_settings_alter() allows modules
* and themes to dynamically alter a library definition (once per request).
*
* @var array
*/
protected $libraryDefinitions = [];
/**
* Constructs a new LibraryDiscovery instance.
*
* @param \Drupal\Core\Cache\CacheCollectorInterface $library_discovery_collector
* The library discovery cache collector.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator
* The cache tag invalidator.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
*/
public function __construct(CacheCollectorInterface $library_discovery_collector, CacheTagsInvalidatorInterface $cache_tag_invalidator) {
$this->collector = $library_discovery_collector;
$this->cacheTagInvalidator = $cache_tag_invalidator;
}
/**
* {@inheritdoc}
*/
public function getLibrariesByExtension($extension) {
if (!isset($this->libraryDefinitions[$extension])) {
$libraries = $this->collector->get($extension);
$this->libraryDefinitions[$extension] = [];
foreach ($libraries as $name => $definition) {
$library_name = "$extension/$name";
$this->libraryDefinitions[$extension][$name] = $definition;
}
}
return $this->libraryDefinitions[$extension];
}
/**
* {@inheritdoc}
*/
public function getLibraryByName($extension, $name) {
$extension = $this->getLibrariesByExtension($extension);
return isset($extension[$name]) ? $extension[$name] : FALSE;
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
$this->cacheTagInvalidator->invalidateTags(['library_info']);
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDiscoveryCollector.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* A CacheCollector implementation for building library extension info.
*/
class LibraryDiscoveryCollector extends CacheCollector {
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* The library discovery parser.
*
* @var \Drupal\Core\Asset\LibraryDiscoveryParser
*/
protected $discoveryParser;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* Constructs a CacheCollector object.
*
* @param string $cid
* The cid for the array being cached.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Asset\LibraryDiscoveryParser $discovery_parser
* The library discovery parser.
*/
public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, LibraryDiscoveryParser $discovery_parser, ThemeManagerInterface $theme_manager) {
$this->themeManager = $theme_manager;
parent::__construct(NULL, $cache, $lock, ['library_info']);
$this->discoveryParser = $discovery_parser;
}
/**
* {@inheritdoc}
*/
protected function getCid() {
if (!isset($this->cid)) {
$this->cid = 'library_info:' . $this->themeManager->getActiveTheme()->getName();
}
return $this->cid;
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($key) {
$this->storage[$key] = $this->discoveryParser->buildByExtension($key);
$this->persist($key);
return $this->storage[$key];
}
}

View file

@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDiscoveryInterface.
*/
namespace Drupal\Core\Asset;
/**
* Discovers information for asset (CSS/JavaScript) libraries.
*
* Library information is statically cached. Libraries are keyed by extension
* for several reasons:
* - Libraries are not unique. Multiple extensions might ship with the same
* library in a different version or variant. This registry cannot (and does
* not attempt to) prevent library conflicts.
* - Extensions implementing and thereby depending on a library that is
* registered by another extension can only rely on that extension's library.
* - Two (or more) extensions can still register the same library and use it
* without conflicts in case the libraries are loaded on certain pages only.
*/
interface LibraryDiscoveryInterface {
/**
* Gets all libraries defined by an extension.
*
* @param string $extension
* The name of the extension that registered a library.
*
* @return array
* An associative array of libraries registered by $extension is returned
* (which may be empty).
*
* @see self::getLibraryByName()
*/
public function getLibrariesByExtension($extension);
/**
* Gets a single library defined by an extension by name.
*
* @param string $extension
* The name of the extension that registered a library.
* @param string $name
* The name of a registered library to retrieve.
*
* @return array|FALSE
* The definition of the requested library, if $name was passed and it
* exists, otherwise FALSE.
*/
public function getLibraryByName($extension, $name);
/**
* Clears static and persistent library definition caches.
*/
public function clearCachedDefinitions();
}

View file

@ -0,0 +1,330 @@
<?php
/**
* @file
* Contains \Drupal\Core\Asset\LibraryDiscoveryParser.
*/
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
/**
* Parses library files to get extension data.
*/
class LibraryDiscoveryParser {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new LibraryDiscoveryParser instance.
*
* @param string $root
* The app root.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
$this->root = $root;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
}
/**
* Parses and builds up all the libraries information of an extension.
*
* @param string $extension
* The name of the extension that registered a library.
*
* @return array
* All library definitions of the passed extension.
*
* @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
* Thrown when a library has no js/css/setting.
* @throws \UnexpectedValueException
* Thrown when a js file defines a positive weight.
*/
public function buildByExtension($extension) {
$libraries = array();
if ($extension === 'core') {
$path = 'core';
$extension_type = 'core';
}
else {
if ($this->moduleHandler->moduleExists($extension)) {
$extension_type = 'module';
}
else {
$extension_type = 'theme';
}
$path = $this->drupalGetPath($extension_type, $extension);
}
$libraries = $this->parseLibraryInfo($extension, $path);
foreach ($libraries as $id => &$library) {
if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension));
}
$library += array('dependencies' => array(), 'js' => array(), 'css' => array());
if (isset($library['header']) && !is_bool($library['header'])) {
throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension));
}
if (isset($library['version'])) {
// @todo Retrieve version of a non-core extension.
if ($library['version'] === 'VERSION') {
$library['version'] = \Drupal::VERSION;
}
// Remove 'v' prefix from external library versions.
elseif ($library['version'][0] === 'v') {
$library['version'] = substr($library['version'], 1);
}
}
// If this is a 3rd party library, the license info is required.
if (isset($library['remote']) && !isset($library['license'])) {
throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for definition '%s' extension '%s': it has a remote, but no license.", $id, $extension));
}
// Assign Drupal's license to libraries that don't have license info.
if (!isset($library['license'])) {
$library['license'] = array(
'name' => 'GNU-GPL-2.0-or-later',
'url' => 'https://www.drupal.org/licensing/faq',
'gpl-compatible' => TRUE,
);
}
foreach (array('js', 'css') as $type) {
// Prepare (flatten) the SMACSS-categorized definitions.
// @todo After Asset(ic) changes, retain the definitions as-is and
// properly resolve dependencies for all (css) libraries per category,
// and only once prior to rendering out an HTML page.
if ($type == 'css' && !empty($library[$type])) {
foreach ($library[$type] as $category => $files) {
foreach ($files as $source => $options) {
if (!isset($options['weight'])) {
$options['weight'] = 0;
}
// Apply the corresponding weight defined by CSS_* constants.
$options['weight'] += constant('CSS_' . strtoupper($category));
$library[$type][$source] = $options;
}
unset($library[$type][$category]);
}
}
foreach ($library[$type] as $source => $options) {
unset($library[$type][$source]);
// Allow to omit the options hashmap in YAML declarations.
if (!is_array($options)) {
$options = array();
}
if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library.");
}
// Unconditionally apply default groups for the defined asset files.
// The library system is a dependency management system. Each library
// properly specifies its dependencies instead of relying on a custom
// processing order.
if ($type == 'js') {
$options['group'] = JS_LIBRARY;
}
elseif ($type == 'css') {
$options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
}
// By default, all library assets are files.
if (!isset($options['type'])) {
$options['type'] = 'file';
}
if ($options['type'] == 'external') {
$options['data'] = $source;
}
// Determine the file asset URI.
else {
if ($source[0] === '/') {
// An absolute path maps to DRUPAL_ROOT / base_path().
if ($source[1] !== '/') {
$options['data'] = substr($source, 1);
}
// A protocol-free URI (e.g., //cdn.com/example.js) is external.
else {
$options['type'] = 'external';
$options['data'] = $source;
}
}
// A stream wrapper URI (e.g., public://generated_js/example.js).
elseif ($this->fileValidUri($source)) {
$options['data'] = $source;
}
// By default, file paths are relative to the registering extension.
else {
$options['data'] = $path . '/' . $source;
}
}
if (!isset($library['version'])) {
// @todo Get the information from the extension.
$options['version'] = -1;
}
else {
$options['version'] = $library['version'];
}
// Set the 'minified' flag on JS file assets, default to FALSE.
if ($type == 'js' && $options['type'] == 'file') {
$options['minified'] = isset($options['minified']) ? $options['minified'] : FALSE;
}
$library[$type][] = $options;
}
}
}
return $libraries;
}
/**
* Parses a given library file and allows modules and themes to alter it.
*
* This method sets the parsed information onto the library property.
*
* Library information is parsed from *.libraries.yml files; see
* editor.library.yml for an example. Every library must have at least one js
* or css entry. Each entry starts with a machine name and defines the
* following elements:
* - js: A list of JavaScript files to include. Each file is keyed by the file
* path. An item can have several attributes (like HTML
* attributes). For example:
* @code
* js:
* path/js/file.js: { attributes: { defer: true } }
* @endcode
* If the file has no special attributes, just use an empty object:
* @code
* js:
* path/js/file.js: {}
* @endcode
* The path of the file is relative to the module or theme directory, unless
* it starts with a /, in which case it is relative to the Drupal root. If
* the file path starts with //, it will be treated as a protocol-free,
* external resource (e.g., //cdn.com/library.js). Full URLs
* (e.g., http://cdn.com/library.js) as well as URLs that use a valid
* stream wrapper (e.g., public://path/to/file.js) are also supported.
* - css: A list of categories for which the library provides CSS files. The
* available categories are:
* - base
* - layout
* - component
* - state
* - theme
* Each category is itself a key for a sub-list of CSS files to include:
* @code
* css:
* component:
* css/file.css: {}
* @endcode
* Just like with JavaScript files, each CSS file is the key of an object
* that can define specific attributes. The format of the file path is the
* same as for the JavaScript files.
* - dependencies: A list of libraries this library depends on.
* - version: The library version. The string "VERSION" can be used to mean
* the current Drupal core version.
* - header: By default, JavaScript files are included in the footer. If the
* script must be included in the header (along with all its dependencies),
* set this to true. Defaults to false.
* - minified: If the file is already minified, set this to true to avoid
* minifying it again. Defaults to false.
* - remote: If the library is a third-party script, this provides the
* repository URL for reference.
* - license: If the remote property is set, the license information is
* required. It has 3 properties:
* - name: The human-readable name of the license.
* - url: The URL of the license file/information for the version of the
* library used.
* - gpl-compatible: A Boolean for whether this library is GPL compatible.
*
* See https://www.drupal.org/node/2274843#define-library for more
* information.
*
* @param string $extension
* The name of the extension that registered a library.
* @param string $path
* The relative path to the extension.
*
* @return array
* An array of parsed library data.
*
* @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
* Thrown when a parser exception got thrown.
*/
protected function parseLibraryInfo($extension, $path) {
$libraries = [];
$library_file = $path . '/' . $extension . '.libraries.yml';
if (file_exists($this->root . '/' . $library_file)) {
try {
$libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file));
}
catch (InvalidDataTypeException $e) {
// Rethrow a more helpful exception to provide context.
throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
}
}
// Allow modules to add dynamic library definitions.
$hook = 'library_info_build';
if ($this->moduleHandler->implementsHook($extension, $hook)) {
$libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook));
}
// Allow modules to alter the module's registered libraries.
$this->moduleHandler->alter('library_info', $libraries, $extension);
$this->themeManager->alter('library_info', $libraries, $extension);
return $libraries;
}
/**
* Wraps drupal_get_path().
*/
protected function drupalGetPath($type, $name) {
return drupal_get_path($type, $name);
}
/**
* Wraps file_valid_uri().
*/
protected function fileValidUri($source) {
return file_valid_uri($source);
}
}