1<?php
2
3namespace Drupal\Core\Asset;
4
5use Drupal\Core\File\FileSystemInterface;
6use Drupal\Core\State\StateInterface;
7
8/**
9 * Optimizes JavaScript assets.
10 */
11class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
12
13  /**
14   * A JS asset grouper.
15   *
16   * @var \Drupal\Core\Asset\JsCollectionGrouper
17   */
18  protected $grouper;
19
20  /**
21   * A JS asset optimizer.
22   *
23   * @var \Drupal\Core\Asset\JsOptimizer
24   */
25  protected $optimizer;
26
27  /**
28   * An asset dumper.
29   *
30   * @var \Drupal\Core\Asset\AssetDumper
31   */
32  protected $dumper;
33
34  /**
35   * The state key/value store.
36   *
37   * @var \Drupal\Core\State\StateInterface
38   */
39  protected $state;
40
41  /**
42   * The file system service.
43   *
44   * @var \Drupal\Core\File\FileSystemInterface
45   */
46  protected $fileSystem;
47
48  /**
49   * Constructs a JsCollectionOptimizer.
50   *
51   * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper
52   *   The grouper for JS assets.
53   * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer
54   *   The optimizer for a single JS asset.
55   * @param \Drupal\Core\Asset\AssetDumperInterface $dumper
56   *   The dumper for optimized JS assets.
57   * @param \Drupal\Core\State\StateInterface $state
58   *   The state key/value store.
59   * @param \Drupal\Core\File\FileSystemInterface $file_system
60   *   The file system service.
61   */
62  public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, FileSystemInterface $file_system) {
63    $this->grouper = $grouper;
64    $this->optimizer = $optimizer;
65    $this->dumper = $dumper;
66    $this->state = $state;
67    $this->fileSystem = $file_system;
68  }
69
70  /**
71   * {@inheritdoc}
72   *
73   * The cache file name is retrieved on a page load via a lookup variable that
74   * contains an associative array. The array key is the hash of the names in
75   * $files while the value is the cache file name. The cache file is generated
76   * in two cases. First, if there is no file name value for the key, which will
77   * happen if a new file name has been added to $files or after the lookup
78   * variable is emptied to force a rebuild of the cache. Second, the cache file
79   * is generated if it is missing on disk. Old cache files are not deleted
80   * immediately when the lookup variable is emptied, but are deleted after a
81   * configurable period (@code system.performance.stale_file_threshold @endcode)
82   * to ensure that files referenced by a cached page will still be available.
83   */
84  public function optimize(array $js_assets) {
85    // Group the assets.
86    $js_groups = $this->grouper->group($js_assets);
87
88    // Now optimize (concatenate, not minify) and dump each asset group, unless
89    // that was already done, in which case it should appear in
90    // system.js_cache_files.
91    // Drupal contrib can override this default JS aggregator to keep the same
92    // grouping, optimizing and dumping, but change the strategy that is used to
93    // determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …).
94    $map = $this->state->get('system.js_cache_files', []);
95    $js_assets = [];
96    foreach ($js_groups as $order => $js_group) {
97      // We have to return a single asset, not a group of assets. It is now up
98      // to one of the pieces of code in the switch statement below to set the
99      // 'data' property to the appropriate value.
100      $js_assets[$order] = $js_group;
101      unset($js_assets[$order]['items']);
102
103      switch ($js_group['type']) {
104        case 'file':
105          // No preprocessing, single JS asset: just use the existing URI.
106          if (!$js_group['preprocess']) {
107            $uri = $js_group['items'][0]['data'];
108            $js_assets[$order]['data'] = $uri;
109          }
110          // Preprocess (aggregate), unless the aggregate file already exists.
111          else {
112            $key = $this->generateHash($js_group);
113            $uri = '';
114            if (isset($map[$key])) {
115              $uri = $map[$key];
116            }
117            if (empty($uri) || !file_exists($uri)) {
118              // Concatenate each asset within the group.
119              $data = '';
120              foreach ($js_group['items'] as $js_asset) {
121                // Optimize this JS file, but only if it's not yet minified.
122                if (isset($js_asset['minified']) && $js_asset['minified']) {
123                  $data .= file_get_contents($js_asset['data']);
124                }
125                else {
126                  $data .= $this->optimizer->optimize($js_asset);
127                }
128                // Append a ';' and a newline after each JS file to prevent them
129                // from running together.
130                $data .= ";\n";
131              }
132              // Remove unwanted JS code that cause issues.
133              $data = $this->optimizer->clean($data);
134              // Dump the optimized JS for this group into an aggregate file.
135              $uri = $this->dumper->dump($data, 'js');
136              // Set the URI for this group's aggregate file.
137              $js_assets[$order]['data'] = $uri;
138              // Persist the URI for this aggregate file.
139              $map[$key] = $uri;
140              $this->state->set('system.js_cache_files', $map);
141            }
142            else {
143              // Use the persisted URI for the optimized JS file.
144              $js_assets[$order]['data'] = $uri;
145            }
146            $js_assets[$order]['preprocessed'] = TRUE;
147          }
148          break;
149
150        case 'external':
151          // We don't do any aggregation and hence also no caching for external
152          // JS assets.
153          $uri = $js_group['items'][0]['data'];
154          $js_assets[$order]['data'] = $uri;
155          break;
156      }
157    }
158
159    return $js_assets;
160  }
161
162  /**
163   * Generate a hash for a given group of JavaScript assets.
164   *
165   * @param array $js_group
166   *   A group of JavaScript assets.
167   *
168   * @return string
169   *   A hash to uniquely identify the given group of JavaScript assets.
170   */
171  protected function generateHash(array $js_group) {
172    $js_data = [];
173    foreach ($js_group['items'] as $js_file) {
174      $js_data[] = $js_file['data'];
175    }
176    return hash('sha256', serialize($js_data));
177  }
178
179  /**
180   * {@inheritdoc}
181   */
182  public function getAll() {
183    return $this->state->get('system.js_cache_files');
184  }
185
186  /**
187   * {@inheritdoc}
188   */
189  public function deleteAll() {
190    $this->state->delete('system.js_cache_files');
191    $delete_stale = function ($uri) {
192      // Default stale file threshold is 30 days.
193      if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
194        $this->fileSystem->delete($uri);
195      }
196    };
197    if (is_dir('public://js')) {
198      $this->fileSystem->scanDirectory('public://js', '/.*/', ['callback' => $delete_stale]);
199    }
200  }
201
202}
203