Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Asset / LibraryDiscoveryParser.php
1 <?php
2
3 namespace Drupal\Core\Asset;
4
5 use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
6 use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
7 use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
8 use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
9 use Drupal\Core\Extension\ModuleHandlerInterface;
10 use Drupal\Core\Serialization\Yaml;
11 use Drupal\Core\Theme\ThemeManagerInterface;
12 use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
13 use Drupal\Component\Utility\NestedArray;
14
15 /**
16  * Parses library files to get extension data.
17  */
18 class LibraryDiscoveryParser {
19
20   /**
21    * The module handler.
22    *
23    * @var \Drupal\Core\Extension\ModuleHandlerInterface
24    */
25   protected $moduleHandler;
26
27   /**
28    * The theme manager.
29    *
30    * @var \Drupal\Core\Theme\ThemeManagerInterface
31    */
32   protected $themeManager;
33
34   /**
35    * The app root.
36    *
37    * @var string
38    */
39   protected $root;
40
41   /**
42    * Constructs a new LibraryDiscoveryParser instance.
43    *
44    * @param string $root
45    *   The app root.
46    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
47    *   The module handler.
48    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
49    *   The theme manager.
50    */
51   public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
52     $this->root = $root;
53     $this->moduleHandler = $module_handler;
54     $this->themeManager = $theme_manager;
55   }
56
57   /**
58    * Parses and builds up all the libraries information of an extension.
59    *
60    * @param string $extension
61    *   The name of the extension that registered a library.
62    *
63    * @return array
64    *   All library definitions of the passed extension.
65    *
66    * @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
67    *   Thrown when a library has no js/css/setting.
68    * @throws \UnexpectedValueException
69    *   Thrown when a js file defines a positive weight.
70    */
71   public function buildByExtension($extension) {
72     $libraries = [];
73
74     if ($extension === 'core') {
75       $path = 'core';
76       $extension_type = 'core';
77     }
78     else {
79       if ($this->moduleHandler->moduleExists($extension)) {
80         $extension_type = 'module';
81       }
82       else {
83         $extension_type = 'theme';
84       }
85       $path = $this->drupalGetPath($extension_type, $extension);
86     }
87
88     $libraries = $this->parseLibraryInfo($extension, $path);
89     $libraries = $this->applyLibrariesOverride($libraries, $extension);
90
91     foreach ($libraries as $id => &$library) {
92       if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
93         throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension));
94       }
95       $library += ['dependencies' => [], 'js' => [], 'css' => []];
96
97       if (isset($library['header']) && !is_bool($library['header'])) {
98         throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension));
99       }
100
101       if (isset($library['version'])) {
102         // @todo Retrieve version of a non-core extension.
103         if ($library['version'] === 'VERSION') {
104           $library['version'] = \Drupal::VERSION;
105         }
106         // Remove 'v' prefix from external library versions.
107         elseif ($library['version'][0] === 'v') {
108           $library['version'] = substr($library['version'], 1);
109         }
110       }
111
112       // If this is a 3rd party library, the license info is required.
113       if (isset($library['remote']) && !isset($library['license'])) {
114         throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for definition '%s' extension '%s': it has a remote, but no license.", $id, $extension));
115       }
116
117       // Assign Drupal's license to libraries that don't have license info.
118       if (!isset($library['license'])) {
119         $library['license'] = [
120           'name' => 'GNU-GPL-2.0-or-later',
121           'url' => 'https://www.drupal.org/licensing/faq',
122           'gpl-compatible' => TRUE,
123         ];
124       }
125
126       foreach (['js', 'css'] as $type) {
127         // Prepare (flatten) the SMACSS-categorized definitions.
128         // @todo After Asset(ic) changes, retain the definitions as-is and
129         //   properly resolve dependencies for all (css) libraries per category,
130         //   and only once prior to rendering out an HTML page.
131         if ($type == 'css' && !empty($library[$type])) {
132           assert('\Drupal\Core\Asset\LibraryDiscoveryParser::validateCssLibrary($library[$type]) < 2', 'CSS files should be specified as key/value pairs, where the values are configuration options. See https://www.drupal.org/node/2274843.');
133           assert('\Drupal\Core\Asset\LibraryDiscoveryParser::validateCssLibrary($library[$type]) === 0', 'CSS must be nested under a category. See https://www.drupal.org/node/2274843.');
134           foreach ($library[$type] as $category => $files) {
135             $category_weight = 'CSS_' . strtoupper($category);
136             assert('defined($category_weight)', 'Invalid CSS category: ' . $category . '. See https://www.drupal.org/node/2274843.');
137             foreach ($files as $source => $options) {
138               if (!isset($options['weight'])) {
139                 $options['weight'] = 0;
140               }
141               // Apply the corresponding weight defined by CSS_* constants.
142               $options['weight'] += constant($category_weight);
143               $library[$type][$source] = $options;
144             }
145             unset($library[$type][$category]);
146           }
147         }
148         foreach ($library[$type] as $source => $options) {
149           unset($library[$type][$source]);
150           // Allow to omit the options hashmap in YAML declarations.
151           if (!is_array($options)) {
152             $options = [];
153           }
154           if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
155             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.");
156           }
157           // Unconditionally apply default groups for the defined asset files.
158           // The library system is a dependency management system. Each library
159           // properly specifies its dependencies instead of relying on a custom
160           // processing order.
161           if ($type == 'js') {
162             $options['group'] = JS_LIBRARY;
163           }
164           elseif ($type == 'css') {
165             $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
166           }
167           // By default, all library assets are files.
168           if (!isset($options['type'])) {
169             $options['type'] = 'file';
170           }
171           if ($options['type'] == 'external') {
172             $options['data'] = $source;
173           }
174           // Determine the file asset URI.
175           else {
176             if ($source[0] === '/') {
177               // An absolute path maps to DRUPAL_ROOT / base_path().
178               if ($source[1] !== '/') {
179                 $options['data'] = substr($source, 1);
180               }
181               // A protocol-free URI (e.g., //cdn.com/example.js) is external.
182               else {
183                 $options['type'] = 'external';
184                 $options['data'] = $source;
185               }
186             }
187             // A stream wrapper URI (e.g., public://generated_js/example.js).
188             elseif ($this->fileValidUri($source)) {
189               $options['data'] = $source;
190             }
191             // A regular URI (e.g., http://example.com/example.js) without
192             // 'external' explicitly specified, which may happen if, e.g.
193             // libraries-override is used.
194             elseif ($this->isValidUri($source)) {
195               $options['type'] = 'external';
196               $options['data'] = $source;
197             }
198             // By default, file paths are relative to the registering extension.
199             else {
200               $options['data'] = $path . '/' . $source;
201             }
202           }
203
204           if (!isset($library['version'])) {
205             // @todo Get the information from the extension.
206             $options['version'] = -1;
207           }
208           else {
209             $options['version'] = $library['version'];
210           }
211
212           // Set the 'minified' flag on JS file assets, default to FALSE.
213           if ($type == 'js' && $options['type'] == 'file') {
214             $options['minified'] = isset($options['minified']) ? $options['minified'] : FALSE;
215           }
216
217           $library[$type][] = $options;
218         }
219       }
220     }
221
222     return $libraries;
223   }
224
225   /**
226    * Parses a given library file and allows modules and themes to alter it.
227    *
228    * This method sets the parsed information onto the library property.
229    *
230    * Library information is parsed from *.libraries.yml files; see
231    * editor.library.yml for an example. Every library must have at least one js
232    * or css entry. Each entry starts with a machine name and defines the
233    * following elements:
234    * - js: A list of JavaScript files to include. Each file is keyed by the file
235    *   path. An item can have several attributes (like HTML
236    *   attributes). For example:
237    *   @code
238    *   js:
239    *     path/js/file.js: { attributes: { defer: true } }
240    *   @endcode
241    *   If the file has no special attributes, just use an empty object:
242    *   @code
243    *   js:
244    *     path/js/file.js: {}
245    *   @endcode
246    *   The path of the file is relative to the module or theme directory, unless
247    *   it starts with a /, in which case it is relative to the Drupal root. If
248    *   the file path starts with //, it will be treated as a protocol-free,
249    *   external resource (e.g., //cdn.com/library.js). Full URLs
250    *   (e.g., http://cdn.com/library.js) as well as URLs that use a valid
251    *   stream wrapper (e.g., public://path/to/file.js) are also supported.
252    * - css: A list of categories for which the library provides CSS files. The
253    *   available categories are:
254    *   - base
255    *   - layout
256    *   - component
257    *   - state
258    *   - theme
259    *   Each category is itself a key for a sub-list of CSS files to include:
260    *   @code
261    *   css:
262    *     component:
263    *       css/file.css: {}
264    *   @endcode
265    *   Just like with JavaScript files, each CSS file is the key of an object
266    *   that can define specific attributes. The format of the file path is the
267    *   same as for the JavaScript files.
268    * - dependencies: A list of libraries this library depends on.
269    * - version: The library version. The string "VERSION" can be used to mean
270    *   the current Drupal core version.
271    * - header: By default, JavaScript files are included in the footer. If the
272    *   script must be included in the header (along with all its dependencies),
273    *   set this to true. Defaults to false.
274    * - minified: If the file is already minified, set this to true to avoid
275    *   minifying it again. Defaults to false.
276    * - remote: If the library is a third-party script, this provides the
277    *   repository URL for reference.
278    * - license: If the remote property is set, the license information is
279    *   required. It has 3 properties:
280    *   - name: The human-readable name of the license.
281    *   - url: The URL of the license file/information for the version of the
282    *     library used.
283    *   - gpl-compatible: A Boolean for whether this library is GPL compatible.
284    *
285    * See https://www.drupal.org/node/2274843#define-library for more
286    * information.
287    *
288    * @param string $extension
289    *   The name of the extension that registered a library.
290    * @param string $path
291    *   The relative path to the extension.
292    *
293    * @return array
294    *   An array of parsed library data.
295    *
296    * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
297    *   Thrown when a parser exception got thrown.
298    */
299   protected function parseLibraryInfo($extension, $path) {
300     $libraries = [];
301
302     $library_file = $path . '/' . $extension . '.libraries.yml';
303     if (file_exists($this->root . '/' . $library_file)) {
304       try {
305         $libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file));
306       }
307       catch (InvalidDataTypeException $e) {
308         // Rethrow a more helpful exception to provide context.
309         throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
310       }
311     }
312
313     // Allow modules to add dynamic library definitions.
314     $hook = 'library_info_build';
315     if ($this->moduleHandler->implementsHook($extension, $hook)) {
316       $libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook));
317     }
318
319     // Allow modules to alter the module's registered libraries.
320     $this->moduleHandler->alter('library_info', $libraries, $extension);
321     $this->themeManager->alter('library_info', $libraries, $extension);
322
323     return $libraries;
324   }
325
326   /**
327    * Apply libraries overrides specified for the current active theme.
328    *
329    * @param array $libraries
330    *   The libraries definitions.
331    * @param string $extension
332    *   The extension in which these libraries are defined.
333    *
334    * @return array
335    *   The modified libraries definitions.
336    */
337   protected function applyLibrariesOverride($libraries, $extension) {
338     $active_theme = $this->themeManager->getActiveTheme();
339     // ActiveTheme::getLibrariesOverride() returns libraries-overrides for the
340     // current theme as well as all its base themes.
341     $all_libraries_overrides = $active_theme->getLibrariesOverride();
342     foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) {
343       foreach ($libraries as $library_name => $library) {
344         // Process libraries overrides.
345         if (isset($libraries_overrides["$extension/$library_name"])) {
346           // Active theme defines an override for this library.
347           $override_definition = $libraries_overrides["$extension/$library_name"];
348           if (is_string($override_definition) || $override_definition === FALSE) {
349             // A string or boolean definition implies an override (or removal)
350             // for the whole library. Use the override key to specify that this
351             // library will be overridden when it is called.
352             // @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName()
353             if ($override_definition) {
354               $libraries[$library_name]['override'] = $override_definition;
355             }
356             else {
357               $libraries[$library_name]['override'] = FALSE;
358             }
359           }
360           elseif (is_array($override_definition)) {
361             // An array definition implies an override for an asset within this
362             // library.
363             foreach ($override_definition as $sub_key => $value) {
364               // Throw an exception if the asset is not properly specified.
365               if (!is_array($value)) {
366                 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"));
367               }
368               if ($sub_key === 'drupalSettings') {
369                 // drupalSettings may not be overridden.
370                 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"));
371               }
372               elseif ($sub_key === 'css') {
373                 // SMACSS category should be incorporated into the asset name.
374                 foreach ($value as $category => $overrides) {
375                   $this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path);
376                 }
377               }
378               else {
379                 $this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path);
380               }
381             }
382           }
383         }
384       }
385     }
386
387     return $libraries;
388   }
389
390   /**
391    * Wraps drupal_get_path().
392    */
393   protected function drupalGetPath($type, $name) {
394     return drupal_get_path($type, $name);
395   }
396
397   /**
398    * Wraps file_valid_uri().
399    */
400   protected function fileValidUri($source) {
401     return file_valid_uri($source);
402   }
403
404   /**
405    * Determines if the supplied string is a valid URI.
406    */
407   protected function isValidUri($string) {
408     return count(explode('://', $string)) === 2;
409   }
410
411   /**
412    * Overrides the specified library asset.
413    *
414    * @param array $library
415    *   The containing library definition.
416    * @param array $sub_key
417    *   An array containing the sub-keys specifying the library asset, e.g.
418    *   @code['js']@endcode or @code['css', 'component']@endcode
419    * @param array $overrides
420    *   Specifies the overrides, this is an array where the key is the asset to
421    *   be overridden while the value is overriding asset.
422    */
423   protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) {
424     foreach ($overrides as $original => $replacement) {
425       // Get the attributes of the asset to be overridden. If the key does
426       // not exist, then throw an exception.
427       $key_exists = NULL;
428       $parents = array_merge($sub_key, [$original]);
429       // Save the attributes of the library asset to be overridden.
430       $attributes = NestedArray::getValue($library, $parents, $key_exists);
431       if ($key_exists) {
432         // Remove asset to be overridden.
433         NestedArray::unsetValue($library, $parents);
434         // No need to replace if FALSE is specified, since that is a removal.
435         if ($replacement) {
436           // Ensure the replacement path is relative to drupal root.
437           $replacement = $this->resolveThemeAssetPath($theme_path, $replacement);
438           $new_parents = array_merge($sub_key, [$replacement]);
439           // Replace with an override if specified.
440           NestedArray::setValue($library, $new_parents, $attributes);
441         }
442       }
443     }
444   }
445
446   /**
447    * Ensures that a full path is returned for an overriding theme asset.
448    *
449    * @param string $theme_path
450    *   The theme or base theme.
451    * @param string $overriding_asset
452    *   The overriding library asset.
453    *
454    * @return string
455    *   A fully resolved theme asset path relative to the Drupal directory.
456    */
457   protected function resolveThemeAssetPath($theme_path, $overriding_asset) {
458     if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) {
459       // The destination is not an absolute path and it's not a URI (e.g.
460       // public://generated_js/example.js or http://example.com/js/my_js.js), so
461       // it's relative to the theme.
462       return '/' . $theme_path . '/' . $overriding_asset;
463     }
464     return $overriding_asset;
465   }
466
467   /**
468    * Validates CSS library structure.
469    *
470    * @param array $library
471    *   The library definition array.
472    *
473    * @return int
474    *   Returns based on validity:
475    *     - 0 if the library definition is valid
476    *     - 1 if the library definition has improper nesting
477    *     - 2 if the library definition specifies files as an array
478    */
479   public static function validateCssLibrary($library) {
480     $categories = [];
481     // Verify options first and return early if invalid.
482     foreach ($library as $category => $files) {
483       if (!is_array($files)) {
484         return 2;
485       }
486       $categories[] = $category;
487       foreach ($files as $source => $options) {
488         if (!is_array($options)) {
489           return 1;
490         }
491       }
492     }
493
494     return 0;
495   }
496
497 }