1<?php
2
3namespace Drupal\Core\Asset;
4
5use Drupal\Component\Utility\Crypt;
6use Drupal\Component\Utility\NestedArray;
7use Drupal\Core\Cache\CacheBackendInterface;
8use Drupal\Core\Extension\ModuleHandlerInterface;
9use Drupal\Core\Language\LanguageManagerInterface;
10use Drupal\Core\Theme\ThemeManagerInterface;
11
12/**
13 * The default asset resolver.
14 */
15class 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
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   *
192   * @return array
193   *   A (possibly optimized) collection of JavaScript assets.
194   */
195  protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
196    $settings = [];
197
198    foreach ($this->getLibrariesToLoad($assets) as $library) {
199      list($extension, $name) = explode('/', $library, 2);
200      $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
201      if (isset($definition['drupalSettings'])) {
202        $settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
203      }
204    }
205
206    return $settings;
207  }
208
209  /**
210   * {@inheritdoc}
211   */
212  public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
213    $theme_info = $this->themeManager->getActiveTheme();
214    // Add the theme name to the cache key since themes may implement
215    // hook_library_info_alter(). Additionally add the current language to
216    // support translation of JavaScript files via hook_js_alter().
217    $libraries_to_load = $this->getLibrariesToLoad($assets);
218    $cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($libraries_to_load)) . (int) (count($assets->getSettings()) > 0) . (int) $optimize;
219
220    if ($cached = $this->cache->get($cid)) {
221      list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
222    }
223    else {
224      $javascript = [];
225      $default_options = [
226        'type' => 'file',
227        'group' => JS_DEFAULT,
228        'weight' => 0,
229        'cache' => TRUE,
230        'preprocess' => TRUE,
231        'attributes' => [],
232        'version' => NULL,
233        'browsers' => [],
234      ];
235
236      // Collect all libraries that contain JS assets and are in the header.
237      $header_js_libraries = [];
238      foreach ($libraries_to_load as $library) {
239        list($extension, $name) = explode('/', $library, 2);
240        $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
241        if (isset($definition['js']) && !empty($definition['header'])) {
242          $header_js_libraries[] = $library;
243        }
244      }
245      // The current list of header JS libraries are only those libraries that
246      // are in the header, but their dependencies must also be loaded for them
247      // to function correctly, so update the list with those.
248      $header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
249
250      foreach ($libraries_to_load as $library) {
251        list($extension, $name) = explode('/', $library, 2);
252        $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
253        if (isset($definition['js'])) {
254          foreach ($definition['js'] as $options) {
255            $options += $default_options;
256
257            // 'scope' is a calculated option, based on which libraries are
258            // marked to be loaded from the header (see above).
259            $options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
260
261            // Preprocess can only be set if caching is enabled and no
262            // attributes are set.
263            $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
264
265            // Always add a tiny value to the weight, to conserve the insertion
266            // order.
267            $options['weight'] += count($javascript) / 1000;
268
269            // Local and external files must keep their name as the associative
270            // key so the same JavaScript file is not added twice.
271            $javascript[$options['data']] = $options;
272          }
273        }
274      }
275
276      // Allow modules and themes to alter the JavaScript assets.
277      $this->moduleHandler->alter('js', $javascript, $assets);
278      $this->themeManager->alter('js', $javascript, $assets);
279
280      // Sort JavaScript assets, so that they appear in the correct order.
281      uasort($javascript, 'static::sort');
282
283      // Prepare the return value: filter JavaScript assets per scope.
284      $js_assets_header = [];
285      $js_assets_footer = [];
286      foreach ($javascript as $key => $item) {
287        if ($item['scope'] == 'header') {
288          $js_assets_header[$key] = $item;
289        }
290        elseif ($item['scope'] == 'footer') {
291          $js_assets_footer[$key] = $item;
292        }
293      }
294
295      if ($optimize) {
296        $collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
297        $js_assets_header = $collection_optimizer->optimize($js_assets_header);
298        $js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
299      }
300
301      // If the core/drupalSettings library is being loaded or is already
302      // loaded, get the JavaScript settings assets, and convert them into a
303      // single "regular" JavaScript asset.
304      $libraries_to_load = $this->getLibrariesToLoad($assets);
305      $settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
306      $settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
307
308      // Initialize settings to FALSE since they are not needed by default. This
309      // distinguishes between an empty array which must still allow
310      // hook_js_settings_alter() to be run.
311      $settings = FALSE;
312      if ($settings_required && $settings_have_changed) {
313        $settings = $this->getJsSettingsAssets($assets);
314        // Allow modules to add cached JavaScript settings.
315        foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
316          $function = $module . '_js_settings_build';
317          $function($settings, $assets);
318        }
319      }
320      $settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
321      $this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
322    }
323
324    if ($settings !== FALSE) {
325      // Attached settings override both library definitions and
326      // hook_js_settings_build().
327      $settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
328      // Allow modules and themes to alter the JavaScript settings.
329      $this->moduleHandler->alter('js_settings', $settings, $assets);
330      $this->themeManager->alter('js_settings', $settings, $assets);
331      // Update the $assets object accordingly, so that it reflects the final
332      // settings.
333      $assets->setSettings($settings);
334      $settings_as_inline_javascript = [
335        'type' => 'setting',
336        'group' => JS_SETTING,
337        'weight' => 0,
338        'browsers' => [],
339        'data' => $settings,
340      ];
341      $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
342      // Prepend to the list of JS assets, to render it first. Preferably in
343      // the footer, but in the header if necessary.
344      if ($settings_in_header) {
345        $js_assets_header = $settings_js_asset + $js_assets_header;
346      }
347      else {
348        $js_assets_footer = $settings_js_asset + $js_assets_footer;
349      }
350    }
351    return [
352      $js_assets_header,
353      $js_assets_footer,
354    ];
355  }
356
357  /**
358   * Sorts CSS and JavaScript resources.
359   *
360   * This sort order helps optimize front-end performance while providing
361   * modules and themes with the necessary control for ordering the CSS and
362   * JavaScript appearing on a page.
363   *
364   * @param $a
365   *   First item for comparison. The compared items should be associative
366   *   arrays of member items.
367   * @param $b
368   *   Second item for comparison.
369   *
370   * @return int
371   */
372  public static function sort($a, $b) {
373    // First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
374    // group appear before items in the CSS_AGGREGATE_THEME group. Modules may
375    // create additional groups by defining their own constants.
376    if ($a['group'] < $b['group']) {
377      return -1;
378    }
379    elseif ($a['group'] > $b['group']) {
380      return 1;
381    }
382    // Finally, order by weight.
383    elseif ($a['weight'] < $b['weight']) {
384      return -1;
385    }
386    elseif ($a['weight'] > $b['weight']) {
387      return 1;
388    }
389    else {
390      return 0;
391    }
392  }
393
394}
395