Newer
Older
Alex Pott
committed
<?php
namespace Drupal\Core\Asset;
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
Alex Pott
committed
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
Alex Pott
committed
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
Alex Pott
committed
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
Alex Pott
committed
/**
* 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;
Alex Pott
committed
/**
* The app root.
*
* @var string
*/
protected $root;
/**
* Constructs a new LibraryDiscoveryParser instance.
*
* @param string $root
* The app root.
Alex Pott
committed
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
Jennifer Hodgdon
committed
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
Alex Pott
committed
*/
public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
$this->root = $root;
Alex Pott
committed
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
Alex Pott
committed
}
/**
* 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);
$libraries = $this->applyLibrariesOverride($libraries, $extension);
Alex Pott
committed
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));
Alex Pott
committed
}
$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));
}
Alex Pott
committed
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,
);
}
Alex Pott
committed
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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;
}
// A regular URI (e.g., http://example.com/example.js) without
// 'external' explicitly specified, which may happen if, e.g.
// libraries-override is used.
elseif ($this->isValidUri($source)) {
$options['type'] = 'external';
$options['data'] = $source;
}
Alex Pott
committed
// 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;
}
Alex Pott
committed
$library[$type][] = $options;
}
}
}
return $libraries;
}
/**
Jennifer Hodgdon
committed
* Parses a given library file and allows modules and themes to alter it.
Alex Pott
committed
*
* This method sets the parsed information onto the library property.
*
Angie Byron
committed
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
* 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.
*
Alex Pott
committed
* @param string $extension
* The name of the extension that registered a library.
* @param string $path
* The relative path to the extension.
Alex Pott
committed
*
* @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);
}
Alex Pott
committed
}
// 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));
Alex Pott
committed
}
Alex Pott
committed
// Allow modules to alter the module's registered libraries.
$this->moduleHandler->alter('library_info', $libraries, $extension);
$this->themeManager->alter('library_info', $libraries, $extension);
Alex Pott
committed
return $libraries;
}
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
/**
* Apply libraries overrides specified for the current active theme.
*
* @param array $libraries
* The libraries definitions.
* @param string $extension
* The extension in which these libraries are defined.
*
* @return array
* The modified libraries definitions.
*/
protected function applyLibrariesOverride($libraries, $extension) {
$active_theme = $this->themeManager->getActiveTheme();
// ActiveTheme::getLibrariesOverride() returns libraries-overrides for the
// current theme as well as all its base themes.
$all_libraries_overrides = $active_theme->getLibrariesOverride();
foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) {
foreach ($libraries as $library_name => $library) {
// Process libraries overrides.
if (isset($libraries_overrides["$extension/$library_name"])) {
// Active theme defines an override for this library.
$override_definition = $libraries_overrides["$extension/$library_name"];
if (is_string($override_definition) || $override_definition === FALSE) {
// A string or boolean definition implies an override (or removal)
// for the whole library. Use the override key to specify that this
// library will be overridden when it is called.
// @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName()
if ($override_definition) {
$libraries[$library_name]['override'] = $override_definition;
}
else {
$libraries[$library_name]['override'] = FALSE;
}
}
elseif (is_array($override_definition)) {
// An array definition implies an override for an asset within this
// library.
foreach ($override_definition as $sub_key => $value) {
// Throw an exception if the asset is not properly specified.
if (!is_array($value)) {
throw new InvalidLibrariesOverrideSpecificationException(sprintf('Library asset %s is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".', "$extension/$library_name/$sub_key"));
}
if ($sub_key === 'drupalSettings') {
// drupalSettings may not be overridden.
throw new InvalidLibrariesOverrideSpecificationException(sprintf('drupalSettings may not be overridden in libraries-override. Trying to override %s. Use hook_library_info_alter() instead.', "$extension/$library_name/$sub_key"));
}
elseif ($sub_key === 'css') {
// SMACSS category should be incorporated into the asset name.
foreach ($value as $category => $overrides) {
$this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path);
}
}
else {
$this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path);
}
}
}
}
}
}
return $libraries;
}
Alex Pott
committed
/**
* 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);
}
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/**
* Determines if the supplied string is a valid URI.
*/
protected function isValidUri($string) {
return count(explode('://', $string)) === 2;
}
/**
* Overrides the specified library asset.
*
* @param array $library
* The containing library definition.
* @param array $sub_key
* An array containing the sub-keys specifying the library asset, e.g.
* @code['js']@endcode or @code['css', 'component']@endcode
* @param array $overrides
* Specifies the overrides, this is an array where the key is the asset to
* be overridden while the value is overriding asset.
*/
protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) {
foreach ($overrides as $original => $replacement) {
// Get the attributes of the asset to be overridden. If the key does
// not exist, then throw an exception.
$key_exists = NULL;
$parents = array_merge($sub_key, [$original]);
// Save the attributes of the library asset to be overridden.
$attributes = NestedArray::getValue($library, $parents, $key_exists);
if ($key_exists) {
// Remove asset to be overridden.
NestedArray::unsetValue($library, $parents);
// No need to replace if FALSE is specified, since that is a removal.
if ($replacement) {
// Ensure the replacement path is relative to drupal root.
$replacement = $this->resolveThemeAssetPath($theme_path, $replacement);
$new_parents = array_merge($sub_key, [$replacement]);
// Replace with an override if specified.
NestedArray::setValue($library, $new_parents, $attributes);
}
}
}
}
/**
* Ensures that a full path is returned for an overriding theme asset.
*
* @param string $theme_path
* The theme or base theme.
* @param string $overriding_asset
* The overriding library asset.
*
* @return string
* A fully resolved theme asset path relative to the Drupal directory.
*/
protected function resolveThemeAssetPath($theme_path, $overriding_asset) {
if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) {
// The destination is not an absolute path and it's not a URI (e.g.
// public://generated_js/example.js or http://example.com/js/my_js.js), so
// it's relative to the theme.
return '/' . $theme_path . '/' . $overriding_asset;
}
return $overriding_asset;
}