863f971838ae79f698f9bde4d30904c04eb1bc02
[yaffs-website] / web / core / lib / Drupal / Core / Asset / AssetResolver.php
1 <?php
2
3 namespace Drupal\Core\Asset;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Core\Cache\CacheBackendInterface;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Language\LanguageManagerInterface;
10 use Drupal\Core\Theme\ThemeManagerInterface;
11
12 /**
13  * The default asset resolver.
14  */
15 class AssetResolver implements AssetResolverInterface {
16
17   /**
18    * The library discovery service.
19    *
20    * @var \Drupal\Core\Asset\LibraryDiscoveryInterface
21    */
22   protected $libraryDiscovery;
23
24   /**
25    * The library dependency resolver.
26    *
27    * @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
28    */
29   protected $libraryDependencyResolver;
30
31   /**
32    * The module handler.
33    *
34    * @var \Drupal\Core\Extension\ModuleHandlerInterface
35    */
36   protected $moduleHandler;
37
38   /**
39    * The theme manager.
40    *
41    * @var \Drupal\Core\Theme\ThemeManagerInterface
42    */
43   protected $themeManager;
44
45   /**
46    * The language manager.
47    *
48    * @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
49    */
50   protected $languageManager;
51
52   /**
53    * The cache backend.
54    *
55    * @var \Drupal\Core\Cache\CacheBackendInterface
56    */
57   protected $cache;
58
59   /**
60    * Constructs a new AssetResolver instance.
61    *
62    * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
63    *   The library discovery service.
64    * @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
65    *   The library dependency resolver.
66    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
67    *   The module handler.
68    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
69    *   The theme manager.
70    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
71    *   The language manager.
72    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
73    *   The cache backend.
74    */
75   public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
76     $this->libraryDiscovery = $library_discovery;
77     $this->libraryDependencyResolver = $library_dependency_resolver;
78     $this->moduleHandler = $module_handler;
79     $this->themeManager = $theme_manager;
80     $this->languageManager = $language_manager;
81     $this->cache = $cache;
82   }
83
84   /**
85    * Returns the libraries that need to be loaded.
86    *
87    * For example, with core/a depending on core/c and core/b on core/d:
88    * @code
89    * $assets = new AttachedAssets();
90    * $assets->setLibraries(['core/a', 'core/b', 'core/c']);
91    * $assets->setAlreadyLoadedLibraries(['core/c']);
92    * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
93    * @endcode
94    *
95    * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
96    *   The assets attached to the current response.
97    *
98    * @return string[]
99    *   A list of libraries and their dependencies, in the order they should be
100    *   loaded, excluding any libraries that have already been loaded.
101    */
102   protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
103     return array_diff(
104       $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
105       $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
106     );
107   }
108
109   /**
110    * {@inheritdoc}
111    */
112   public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
113     $theme_info = $this->themeManager->getActiveTheme();
114     // Add the theme name to the cache key since themes may implement
115     // hook_library_info_alter().
116     $libraries_to_load = $this->getLibrariesToLoad($assets);
117     $cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) $optimize;
118     if ($cached = $this->cache->get($cid)) {
119       return $cached->data;
120     }
121
122     $css = [];
123     $default_options = [
124       'type' => 'file',
125       'group' => CSS_AGGREGATE_DEFAULT,
126       'weight' => 0,
127       'media' => 'all',
128       'preprocess' => TRUE,
129       'browsers' => [],
130     ];
131
132     foreach ($libraries_to_load as $library) {
133       list($extension, $name) = explode('/', $library, 2);
134       $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
135       if (isset($definition['css'])) {
136         foreach ($definition['css'] as $options) {
137           $options += $default_options;
138           $options['browsers'] += [
139             'IE' => TRUE,
140             '!IE' => TRUE,
141           ];
142
143           // Files with a query string cannot be preprocessed.
144           if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
145             $options['preprocess'] = FALSE;
146           }
147
148           // Always add a tiny value to the weight, to conserve the insertion
149           // order.
150           $options['weight'] += count($css) / 1000;
151
152           // CSS files are being keyed by the full path.
153           $css[$options['data']] = $options;
154         }
155       }
156     }
157
158     // Allow modules and themes to alter the CSS assets.
159     $this->moduleHandler->alter('css', $css, $assets);
160     $this->themeManager->alter('css', $css, $assets);
161
162     // Sort CSS items, so that they appear in the correct order.
163     uasort($css, 'static::sort');
164
165     // Allow themes to remove CSS files by CSS files full path and file name.
166     // @todo Remove in Drupal 9.0.x.
167     if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
168       foreach ($css as $key => $options) {
169         if (isset($stylesheet_remove[$key])) {
170           unset($css[$key]);
171         }
172       }
173     }
174
175     if ($optimize) {
176       $css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
177     }
178     $this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
179
180     return $css;
181   }
182
183   /**
184    * Returns the JavaScript settings assets for this response's libraries.
185    *
186    * Gathers all drupalSettings from all libraries in the attached assets
187    * collection and merges them.
188    *
189    * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
190    *   The assets attached to the current response.
191    * @return array
192    *   A (possibly optimized) collection of JavaScript assets.
193    */
194   protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
195     $settings = [];
196
197     foreach ($this->getLibrariesToLoad($assets) as $library) {
198       list($extension, $name) = explode('/', $library, 2);
199       $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
200       if (isset($definition['drupalSettings'])) {
201         $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
202       }
203     }
204
205     return $settings;
206   }
207
208   /**
209    * {@inheritdoc}
210    */
211   public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
212     $theme_info = $this->themeManager->getActiveTheme();
213     // Add the theme name to the cache key since themes may implement
214     // hook_library_info_alter(). Additionally add the current language to
215     // support translation of JavaScript files via hook_js_alter().
216     $libraries_to_load = $this->getLibrariesToLoad($assets);
217     $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
218
219     if ($cached = $this->cache->get($cid)) {
220       list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
221     }
222     else {
223       $javascript = [];
224       $default_options = [
225         'type' => 'file',
226         'group' => JS_DEFAULT,
227         'weight' => 0,
228         'cache' => TRUE,
229         'preprocess' => TRUE,
230         'attributes' => [],
231         'version' => NULL,
232         'browsers' => [],
233       ];
234
235       // Collect all libraries that contain JS assets and are in the header.
236       $header_js_libraries = [];
237       foreach ($libraries_to_load as $library) {
238         list($extension, $name) = explode('/', $library, 2);
239         $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
240         if (isset($definition['js']) && !empty($definition['header'])) {
241           $header_js_libraries[] = $library;
242         }
243       }
244       // The current list of header JS libraries are only those libraries that
245       // are in the header, but their dependencies must also be loaded for them
246       // to function correctly, so update the list with those.
247       $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
248
249       foreach ($libraries_to_load as $library) {
250         list($extension, $name) = explode('/', $library, 2);
251         $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
252         if (isset($definition['js'])) {
253           foreach ($definition['js'] as $options) {
254             $options += $default_options;
255
256             // 'scope' is a calculated option, based on which libraries are
257             // marked to be loaded from the header (see above).
258             $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
259
260             // Preprocess can only be set if caching is enabled and no
261             // attributes are set.
262             $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
263
264             // Always add a tiny value to the weight, to conserve the insertion
265             // order.
266             $options['weight'] += count($javascript) / 1000;
267
268             // Local and external files must keep their name as the associative
269             // key so the same JavaScript file is not added twice.
270             $javascript[$options['data']] = $options;
271           }
272         }
273       }
274
275       // Allow modules and themes to alter the JavaScript assets.
276       $this->moduleHandler->alter('js', $javascript, $assets);
277       $this->themeManager->alter('js', $javascript, $assets);
278
279       // Sort JavaScript assets, so that they appear in the correct order.
280       uasort($javascript, 'static::sort');
281
282       // Prepare the return value: filter JavaScript assets per scope.
283       $js_assets_header = [];
284       $js_assets_footer = [];
285       foreach ($javascript as $key => $item) {
286         if ($item['scope'] == 'header') {
287           $js_assets_header[$key] = $item;
288         }
289         elseif ($item['scope'] == 'footer') {
290           $js_assets_footer[$key] = $item;
291         }
292       }
293
294       if ($optimize) {
295         $collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
296         $js_assets_header = $collection_optimizer->optimize($js_assets_header);
297         $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
298       }
299
300       // If the core/drupalSettings library is being loaded or is already
301       // loaded, get the JavaScript settings assets, and convert them into a
302       // single "regular" JavaScript asset.
303       $libraries_to_load = $this->getLibrariesToLoad($assets);
304       $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
305       $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
306
307       // Initialize settings to FALSE since they are not needed by default. This
308       // distinguishes between an empty array which must still allow
309       // hook_js_settings_alter() to be run.
310       $settings = FALSE;
311       if ($settings_required && $settings_have_changed) {
312         $settings = $this->getJsSettingsAssets($assets);
313         // Allow modules to add cached JavaScript settings.
314         foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
315           $function = $module . '_' . 'js_settings_build';
316           $function($settings, $assets);
317         }
318       }
319       $settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
320       $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
321     }
322
323     if ($settings !== FALSE) {
324       // Attached settings override both library definitions and
325       // hook_js_settings_build().
326       $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
327       // Allow modules and themes to alter the JavaScript settings.
328       $this->moduleHandler->alter('js_settings', $settings, $assets);
329       $this->themeManager->alter('js_settings', $settings, $assets);
330       // Update the $assets object accordingly, so that it reflects the final
331       // settings.
332       $assets->setSettings($settings);
333       $settings_as_inline_javascript = [
334         'type' => 'setting',
335         'group' => JS_SETTING,
336         'weight' => 0,
337         'browsers' => [],
338         'data' => $settings,
339       ];
340       $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
341       // Prepend to the list of JS assets, to render it first. Preferably in
342       // the footer, but in the header if necessary.
343       if ($settings_in_header) {
344         $js_assets_header = $settings_js_asset + $js_assets_header;
345       }
346       else {
347         $js_assets_footer = $settings_js_asset + $js_assets_footer;
348       }
349     }
350     return [
351       $js_assets_header,
352       $js_assets_footer,
353     ];
354   }
355
356   /**
357    * Sorts CSS and JavaScript resources.
358    *
359    * This sort order helps optimize front-end performance while providing
360    * modules and themes with the necessary control for ordering the CSS and
361    * JavaScript appearing on a page.
362    *
363    * @param $a
364    *   First item for comparison. The compared items should be associative
365    *   arrays of member items.
366    * @param $b
367    *   Second item for comparison.
368    *
369    * @return int
370    */
371   public static function sort($a, $b) {
372     // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
373     // group appear before items in the CSS_AGGREGATE_THEME group. Modules may
374     // create additional groups by defining their own constants.
375     if ($a['group'] < $b['group']) {
376       return -1;
377     }
378     elseif ($a['group'] > $b['group']) {
379       return 1;
380     }
381     // Finally, order by weight.
382     elseif ($a['weight'] < $b['weight']) {
383       return -1;
384     }
385     elseif ($a['weight'] > $b['weight']) {
386       return 1;
387     }
388     else {
389       return 0;
390     }
391   }
392
393 }