Yaffs site version 1.1
[yaffs-website] / web / modules / contrib / advagg / src / State / Files.php
1 <?php
2
3 namespace Drupal\advagg\State;
4
5 use Drupal\Core\Asset\AssetDumperInterface;
6 use Drupal\Core\Cache\CacheBackendInterface;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
10 use Drupal\Core\Lock\LockBackendInterface;
11 use Drupal\Component\Utility\Crypt;
12
13 /**
14  * Provides AdvAgg with a file status state system using a key value store.
15  */
16 class Files extends State {
17
18   /**
19    * A config object for the advagg configuration.
20    *
21    * @var \Drupal\Core\Config\Config
22    */
23   protected $config;
24
25   /**
26    * Module handler service.
27    *
28    * @var \Drupal\Core\Extension\ModuleHandlerInterface
29    */
30   protected $moduleHandler;
31
32   /**
33    * Save location for split files.
34    *
35    * @var string
36    */
37   protected $partsPath;
38
39   /**
40    * An asset dumper.
41    *
42    * @var \Drupal\Core\Asset\AssetDumper
43    */
44   protected $dumper;
45
46   /**
47    * Constructs the State object.
48    *
49    * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
50    *   The key value store to use.
51    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
52    *   A config factory for retrieving required config objects.
53    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
54    *   The module handler.
55    * @param \Drupal\Core\Asset\AssetDumperInterface $asset_dumper
56    *   The dumper for optimized CSS assets.
57    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
58    *   The cache backend.
59    * @param \Drupal\Core\Lock\LockBackendInterface $lock
60    *   The lock backend.
61    */
62   public function __construct(KeyValueFactoryInterface $key_value_factory, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, AssetDumperInterface $asset_dumper, CacheBackendInterface $cache, LockBackendInterface $lock) {
63     parent::__construct($key_value_factory, $cache, $lock);
64     $this->keyValueStore = $key_value_factory->get('advagg_files');
65     $this->config = $config_factory->get('advagg.settings');
66     $this->moduleHandler = $module_handler;
67     $this->dumper = $asset_dumper;
68     $this->partsPath = $this->dumper->preparePath('css') . 'parts/';
69     file_prepare_directory($this->partsPath, FILE_CREATE_DIRECTORY);
70   }
71
72   /**
73    * Given a filename calculate various hashes and gather meta data.
74    *
75    * @param string $file
76    *   A filename/path.
77    * @param array $cached
78    *   An array of previous values from the cache.
79    * @param string $file_contents
80    *   Contents of the given file.
81    *
82    * @return array
83    *   $data which contains
84    *
85    * @code
86    *   'filesize' => filesize($file),
87    *   'mtime' => @filemtime($file),
88    *   'filename_hash' => Crypt::hashBase64($file),
89    *   'content_hash' => Crypt::hashBase64($file_contents),
90    *   'linecount' => $linecount,
91    *   'data' => $file,
92    *   'fileext' => $ext,
93    *   ...
94    * @endcode
95    */
96   public function scanFile($file, array $cached = [], $file_contents = '') {
97     // Clear PHP's internal file status cache.
98     clearstatcache(TRUE, $file);
99
100     if (empty($file_contents)) {
101       $file_contents = (string) @file_get_contents($file);
102     }
103     $content_hash = Crypt::hashBase64($file_contents);
104     if (!empty($cached) && $content_hash != $cached['content_hash']) {
105       $changes = $cached['changes'] + 1;
106     }
107     else {
108       $changes = 0;
109     }
110     $ext = pathinfo($file, PATHINFO_EXTENSION);
111     if ($ext !== 'css' && $ext !== 'js') {
112       if ($ext === 'less') {
113         $ext = 'css';
114       }
115     }
116
117     if ($ext === 'css') {
118       // Get the number of selectors.
119       // http://stackoverflow.com/a/12567381/125684
120       $linecount = preg_match_all('/\{.+?\}|,/s', $file_contents);
121     }
122     else {
123       // Get the number of lines.
124       $linecount = substr_count($file_contents, "\n");
125     }
126
127     // Build meta data array.
128     $data = [
129       'filesize' => (int) @filesize($file),
130       'mtime' => @filemtime($file),
131       'filename_hash' => Crypt::hashBase64($file),
132       'content_hash' => $content_hash,
133       'linecount' => $linecount,
134       'data' => $file,
135       'fileext' => $ext,
136       'updated' => REQUEST_TIME,
137       'contents' => $file_contents,
138       'changes' => $changes,
139     ];
140
141     if ($ext === 'css' && $linecount > $this->config->get('css.ie.selector_limit')) {
142       $this->splitCssFile($data);
143     }
144
145     // Run hook so other modules can modify the data.
146     // Call hook_advagg_scan_file_alter().
147     $this->moduleHandler->alter('advagg_scan_file', $file, $data, $cached);
148     unset($data['contents']);
149     $this->set($file, $data);
150     return $data;
151   }
152
153   /**
154    * {@inheritdoc}
155    */
156   public function getMultiple(array $keys, $refresh_data = NULL) {
157     $values = [];
158     $load = [];
159     $cache_level = $this->config->get('cache_level');
160     $cache_time = advagg_get_cache_time($cache_level);
161
162     foreach ($keys as $key) {
163       // Check if we have a value in the cache.
164       $value = $this->get($key);
165       if ($value) {
166         $values[$key] = $value;
167       }
168       else {
169         $load[] = $key;
170       }
171     }
172
173     if ($load) {
174       $loaded_values = $this->keyValueStore->getMultiple($load);
175       foreach ($load as $key) {
176         // If we find a value, add it to the temporary cache.
177         if (isset($loaded_values[$key])) {
178           if ($refresh_data === FALSE) {
179             $values[$key] = $loaded_values[$key];
180             $this->set($key, $loaded_values[$key]);
181             continue;
182           }
183           $file_contents = (string) @file_get_contents($key);
184           if (!$refresh_data && $cache_level != -1 && !empty($loaded_values[$key]['updated'])) {
185             // If data last updated too long ago check for changes.
186             // Ensure the file exists.
187             if (!file_exists($key)) {
188               $this->delete($key);
189               $values[$key] = NULL;
190               continue;
191             }
192             // If cache is Normal, check file for changes.
193             if ($cache_level == 1 || REQUEST_TIME - $loaded_values[$key]['updated'] < $cache_time) {
194               $content_hash = Crypt::hashBase64($file_contents);
195               if ($content_hash == $loaded_values[$key]['content_hash']) {
196                 $values[$key] = $loaded_values[$key];
197                 $this->set($key, $loaded_values[$key]);
198                 continue;
199               }
200             }
201           }
202
203           // If file exists but is changed rescan.
204           $values[$key] = $this->scanFile($key, $loaded_values[$key], $file_contents);
205           continue;
206         }
207
208         if (file_exists($key)) {
209           // File has never been scanned, scan it.
210           $values[$key] = $this->scanFile($key);
211         }
212       }
213     }
214
215     return $values;
216   }
217
218   /**
219    * {@inheritdoc}
220    */
221   public function get($key, $default = NULL) {
222     // https://api.drupal.org/api/drupal/core!lib!Drupal!Core!State!State.php/function/State::get/8.3.x
223     // Passthrough for Drupal 8.3+.
224     if (version_compare(\Drupal::VERSION, '8.3.0') >= 0) {
225       return parent::get($key, $default);
226     }
227     // https://api.drupal.org/api/drupal/core!lib!Drupal!Core!State!State.php/function/State::get/8.2.x
228     // Use State::getMultiple vs Files::getMultiple for older Drupal 8 versions.
229     $values = parent::getMultiple([$key]);
230     return isset($values[$key]) ? $values[$key] : $default;
231   }
232
233   /**
234    * Split up a CSS string by @media queries.
235    *
236    * @param string $css
237    *   String of CSS.
238    *
239    * @return array
240    *   array of css with only media queries.
241    *
242    * @see http://stackoverflow.com/questions/14145620/regular-expression-for-media-queries-in-css
243    */
244   private function parseMediaBlocks($css) {
245     $media_blocks = [];
246     $start = 0;
247     $last_start = 0;
248
249     // Using the string as an array throughout this function.
250     // http://php.net/types.string#language.types.string.substr
251     while (($start = strpos($css, "@media", $start)) !== FALSE) {
252       // Stack to manage brackets.
253       $s = [];
254
255       // Get the first opening bracket.
256       $i = strpos($css, "{", $start);
257
258       // If $i is false, then there is probably a css syntax error.
259       if ($i === FALSE) {
260         continue;
261       }
262
263       // Push bracket onto stack.
264       array_push($s, $css[$i]);
265       // Move past first bracket.
266       ++$i;
267
268       // Find the closing bracket for the @media statement. But ensure we don't
269       // overflow if there's an error.
270       while (!empty($s) && isset($css[$i])) {
271         // If the character is an opening bracket, push it onto the stack,
272         // otherwise pop the stack.
273         if ($css[$i] === "{") {
274           array_push($s, "{");
275         }
276         elseif ($css[$i] === "}") {
277           array_pop($s);
278         }
279         ++$i;
280       }
281
282       // Get CSS before @media and store it.
283       if ($last_start != $start) {
284         $insert = trim(substr($css, $last_start, $start - $last_start));
285         if (!empty($insert)) {
286           $media_blocks[] = $insert;
287         }
288       }
289       // Cut @media block out of the css and store.
290       $media_blocks[] = trim(substr($css, $start, $i - $start));
291       // Set the new $start to the end of the block.
292       $start = $i;
293       $last_start = $start;
294     }
295
296     // Add in any remaining css rules after the last @media statement.
297     if (strlen($css) > $last_start) {
298       $insert = trim(substr($css, $last_start));
299       if (!empty($insert)) {
300         $media_blocks[] = $insert;
301       }
302     }
303
304     return $media_blocks;
305   }
306
307   /**
308    * Given a file info array it will split the file up.
309    *
310    * @param array $file_info
311    *   File info array.
312    *
313    * @return array
314    *   Array with file and split data.
315    */
316   private function splitCssFile(array &$file_info) {
317     // Get the CSS file and break up by media queries.
318     if (!isset($file_info['contents'])) {
319       $file_info['contents'] = file_get_contents($file_info['data']);
320     }
321     $media_blocks = $this->parseMediaBlocks($file_info['contents']);
322
323     // Get 98% of the css.ie.selector_limit; usually 4013.
324     $selector_split_value = (int) max(floor($this->config->get('css.ie.selector_limit') * 0.98), 100);
325     $part_selector_count = 0;
326     $major_chunks = [];
327     $counter = 0;
328
329     // Group media queries together.
330     foreach ($media_blocks as $media_block) {
331       // Get the number of selectors.
332       // http://stackoverflow.com/a/12567381/125684
333       $selector_count = preg_match_all('/\{.+?\}|,/s', $media_block);
334       $part_selector_count += $selector_count;
335
336       if ($part_selector_count > $selector_split_value) {
337         if (isset($major_chunks[$counter])) {
338           ++$counter;
339           $major_chunks[$counter] = $media_block;
340         }
341         else {
342           $major_chunks[$counter] = $media_block;
343         }
344         ++$counter;
345         $part_selector_count = 0;
346       }
347       else {
348         if (isset($major_chunks[$counter])) {
349           $major_chunks[$counter] .= "\n" . $media_block;
350         }
351         else {
352           $major_chunks[$counter] = $media_block;
353         }
354       }
355     }
356
357     $file_info['parts'] = [];
358     $overall_split = 0;
359     $split_at = $selector_split_value;
360     $chunk_split_value = (int) $this->config->get('css.ie.selector_limit') - $selector_split_value - 1;
361     foreach ($major_chunks as $chunks) {
362       // Get the number of selectors.
363       $selector_count = preg_match_all('/\{.+?\}|,/s', $chunks);
364
365       // Pass through if selector count is low.
366       if ($selector_count < $selector_split_value) {
367         $overall_split += $selector_count;
368         $subfile = $this->createSubfile($chunks, $overall_split, $file_info);
369         if (!$subfile) {
370           // Somthing broke; do not create a subfile.
371           \Drupal::logger('advagg')->notice('Spliting up a CSS file failed. File info: <code>@info</code>', ['@info' => var_export($file_info, TRUE)]);
372           return [];
373         }
374         $file_info['parts'][] = [
375           'path' => $subfile,
376           'selectors' => $selector_count,
377         ];
378         continue;
379       }
380
381       $media_query = '';
382       if (strpos($chunks, '@media') !== FALSE) {
383         $media_query_pos = strpos($chunks, '{');
384         $media_query = substr($chunks, 0, $media_query_pos);
385         $chunks = substr($chunks, $media_query_pos + 1);
386       }
387
388       // Split CSS into selector chunks.
389       $split = preg_split('/(\{.+?\}|,)/si', $chunks, -1, PREG_SPLIT_DELIM_CAPTURE);
390
391       // Setup and handle media queries.
392       $new_css_chunk = [0 => ''];
393       $selector_chunk_counter = 0;
394       $counter = 0;
395       if (!empty($media_query)) {
396         $new_css_chunk[0] = $media_query . '{';
397         $new_css_chunk[1] = '';
398         ++$selector_chunk_counter;
399         ++$counter;
400       }
401       // Have the key value be the running selector count and put split array
402       // semi back together.
403       foreach ($split as $value) {
404         $new_css_chunk[$counter] .= $value;
405         if (strpos($value, '}') === FALSE) {
406           ++$selector_chunk_counter;
407         }
408         else {
409           if ($counter + 1 < $selector_chunk_counter) {
410             $selector_chunk_counter += ($counter - $selector_chunk_counter + 1) / 2;
411           }
412           $counter = $selector_chunk_counter;
413           if (!isset($new_css_chunk[$counter])) {
414             $new_css_chunk[$counter] = '';
415           }
416         }
417       }
418
419       // Group selectors.
420       while (!empty($new_css_chunk)) {
421         // Find where to split the array.
422         $string_to_write = '';
423         while (array_key_exists($split_at, $new_css_chunk) === FALSE) {
424           --$split_at;
425         }
426
427         // Combine parts of the css so that it can be saved to disk.
428         foreach ($new_css_chunk as $key => $value) {
429           if ($key !== $split_at) {
430             // Move this css row to the $string_to_write variable.
431             $string_to_write .= $value;
432             unset($new_css_chunk[$key]);
433           }
434           // We are at the split point.
435           else {
436             // Get the number of selectors in this chunk.
437             $chunk_selector_count = preg_match_all('/\{.+?\}|,/s', $new_css_chunk[$key]);
438             if ($chunk_selector_count < $chunk_split_value) {
439               // The number of selectors at this point is below the threshold;
440               // move this chunk to the write var and break out of the loop.
441               $string_to_write .= $value;
442               unset($new_css_chunk[$key]);
443               $overall_split = $split_at;
444               $split_at += $selector_split_value;
445             }
446             else {
447               // The number of selectors with this chunk included is over the
448               // threshold; do not move it. Change split position so the next
449               // iteration of the while loop ends at the correct spot. Because
450               // we skip unset here, this chunk will start the next part file.
451               $overall_split = $split_at;
452               $split_at += $selector_split_value - $chunk_selector_count;
453             }
454             break;
455           }
456         }
457
458         // Handle media queries.
459         if (!empty($media_query)) {
460           // See if brackets need a new line.
461           if (strpos($string_to_write, "\n") === 0) {
462             $open_bracket = '{';
463           }
464           else {
465             $open_bracket = "{\n";
466           }
467           if (strrpos($string_to_write, "\n") === strlen($string_to_write)) {
468             $close_bracket = '}';
469           }
470           else {
471             $close_bracket = "\n}";
472           }
473
474           // Fix syntax around media queries.
475           if ($first) {
476             $string_to_write .= $close_bracket;
477           }
478           elseif (empty($new_css_chunk)) {
479             $string_to_write = $media_query . $open_bracket . $string_to_write;
480           }
481           else {
482             $string_to_write = $media_query . $open_bracket . $string_to_write . $close_bracket;
483           }
484         }
485         // Write the data.
486         $subfile = $this->createSubfile($string_to_write, $overall_split, $file_info);
487         if (!$subfile) {
488           // Somthing broke; did not create a subfile.
489           \Drupal::logger('advagg')->notice('Spliting up a CSS file failed. File info: <code>@info</code>', ['@info' => var_export($file_info, TRUE)]);
490           return [];
491         }
492         $sub_selector_count = preg_match_all('/\{.+?\}|,/s', $string_to_write, $matches);
493         $file_info['parts'][] = [
494           'path' => $subfile,
495           'selectors' => $sub_selector_count,
496         ];
497       }
498     }
499   }
500
501   /**
502    * Write CSS parts to disk; used when CSS selectors in one file is > 4096.
503    *
504    * @param string $css
505    *   CSS data to write to disk.
506    * @param int $overall_split
507    *   Running count of what selector we are from the original file.
508    * @param array $file_info
509    *   File info array.
510    *
511    * @return string
512    *   Saved path; FALSE on failure.
513    */
514   private function createSubfile($css, $overall_split, array &$file_info) {
515     // Get the path from $file_info['data'].
516     $file = advagg_get_relative_path($file_info['data']);
517     if (!file_exists($file) || is_dir($file)) {
518       return FALSE;
519     }
520
521     // Write the current chunk of the CSS into a file.
522     $path = $this->partsPath . $file . $overall_split . '.css';
523     $directory = dirname($path);
524     file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
525     file_unmanaged_save_data($css, $path, FILE_EXISTS_REPLACE);
526     if (!file_exists($path)) {
527       return FALSE;
528     }
529
530     return $path;
531   }
532
533 }