3 namespace Drupal\advagg\State;
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;
14 * Provides AdvAgg with a file status state system using a key value store.
16 class Files extends State {
19 * A config object for the advagg configuration.
21 * @var \Drupal\Core\Config\Config
26 * Module handler service.
28 * @var \Drupal\Core\Extension\ModuleHandlerInterface
30 protected $moduleHandler;
33 * Save location for split files.
42 * @var \Drupal\Core\Asset\AssetDumper
47 * Constructs the State object.
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
55 * @param \Drupal\Core\Asset\AssetDumperInterface $asset_dumper
56 * The dumper for optimized CSS assets.
57 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
59 * @param \Drupal\Core\Lock\LockBackendInterface $lock
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);
73 * Given a filename calculate various hashes and gather meta data.
77 * @param array $cached
78 * An array of previous values from the cache.
79 * @param string $file_contents
80 * Contents of the given file.
83 * $data which contains
86 * 'filesize' => filesize($file),
87 * 'mtime' => @filemtime($file),
88 * 'filename_hash' => Crypt::hashBase64($file),
89 * 'content_hash' => Crypt::hashBase64($file_contents),
90 * 'linecount' => $linecount,
96 public function scanFile($file, array $cached = [], $file_contents = '') {
97 // Clear PHP's internal file status cache.
98 clearstatcache(TRUE, $file);
100 if (empty($file_contents)) {
101 $file_contents = (string) @file_get_contents($file);
103 $content_hash = Crypt::hashBase64($file_contents);
104 if (!empty($cached) && $content_hash != $cached['content_hash']) {
105 $changes = $cached['changes'] + 1;
110 $ext = pathinfo($file, PATHINFO_EXTENSION);
111 if ($ext !== 'css' && $ext !== 'js') {
112 if ($ext === 'less') {
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);
123 // Get the number of lines.
124 $linecount = substr_count($file_contents, "\n");
127 // Build meta data array.
129 'filesize' => (int) @filesize($file),
130 'mtime' => @filemtime($file),
131 'filename_hash' => Crypt::hashBase64($file),
132 'content_hash' => $content_hash,
133 'linecount' => $linecount,
136 'updated' => REQUEST_TIME,
137 'contents' => $file_contents,
138 'changes' => $changes,
141 if ($ext === 'css' && $linecount > $this->config->get('css.ie.selector_limit')) {
142 $this->splitCssFile($data);
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);
156 public function getMultiple(array $keys, $refresh_data = NULL) {
159 $cache_level = $this->config->get('cache_level');
160 $cache_time = advagg_get_cache_time($cache_level);
162 foreach ($keys as $key) {
163 // Check if we have a value in the cache.
164 $value = $this->get($key);
166 $values[$key] = $value;
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]);
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)) {
189 $values[$key] = NULL;
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]);
203 // If file exists but is changed rescan.
204 $values[$key] = $this->scanFile($key, $loaded_values[$key], $file_contents);
208 if (file_exists($key)) {
209 // File has never been scanned, scan it.
210 $values[$key] = $this->scanFile($key);
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);
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;
234 * Split up a CSS string by @media queries.
240 * array of css with only media queries.
242 * @see http://stackoverflow.com/questions/14145620/regular-expression-for-media-queries-in-css
244 private function parseMediaBlocks($css) {
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.
255 // Get the first opening bracket.
256 $i = strpos($css, "{", $start);
258 // If $i is false, then there is probably a css syntax error.
263 // Push bracket onto stack.
264 array_push($s, $css[$i]);
265 // Move past first bracket.
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] === "{") {
276 elseif ($css[$i] === "}") {
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;
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.
293 $last_start = $start;
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;
304 return $media_blocks;
308 * Given a file info array it will split the file up.
310 * @param array $file_info
314 * Array with file and split data.
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']);
321 $media_blocks = $this->parseMediaBlocks($file_info['contents']);
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;
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;
336 if ($part_selector_count > $selector_split_value) {
337 if (isset($major_chunks[$counter])) {
339 $major_chunks[$counter] = $media_block;
342 $major_chunks[$counter] = $media_block;
345 $part_selector_count = 0;
348 if (isset($major_chunks[$counter])) {
349 $major_chunks[$counter] .= "\n" . $media_block;
352 $major_chunks[$counter] = $media_block;
357 $file_info['parts'] = [];
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);
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);
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)]);
374 $file_info['parts'][] = [
376 'selectors' => $selector_count,
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);
388 // Split CSS into selector chunks.
389 $split = preg_split('/(\{.+?\}|,)/si', $chunks, -1, PREG_SPLIT_DELIM_CAPTURE);
391 // Setup and handle media queries.
392 $new_css_chunk = [0 => ''];
393 $selector_chunk_counter = 0;
395 if (!empty($media_query)) {
396 $new_css_chunk[0] = $media_query . '{';
397 $new_css_chunk[1] = '';
398 ++$selector_chunk_counter;
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;
409 if ($counter + 1 < $selector_chunk_counter) {
410 $selector_chunk_counter += ($counter - $selector_chunk_counter + 1) / 2;
412 $counter = $selector_chunk_counter;
413 if (!isset($new_css_chunk[$counter])) {
414 $new_css_chunk[$counter] = '';
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) {
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]);
434 // We are at the split point.
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;
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;
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) {
465 $open_bracket = "{\n";
467 if (strrpos($string_to_write, "\n") === strlen($string_to_write)) {
468 $close_bracket = '}';
471 $close_bracket = "\n}";
474 // Fix syntax around media queries.
476 $string_to_write .= $close_bracket;
478 elseif (empty($new_css_chunk)) {
479 $string_to_write = $media_query . $open_bracket . $string_to_write;
482 $string_to_write = $media_query . $open_bracket . $string_to_write . $close_bracket;
486 $subfile = $this->createSubfile($string_to_write, $overall_split, $file_info);
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)]);
492 $sub_selector_count = preg_match_all('/\{.+?\}|,/s', $string_to_write, $matches);
493 $file_info['parts'][] = [
495 'selectors' => $sub_selector_count,
502 * Write CSS parts to disk; used when CSS selectors in one file is > 4096.
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
512 * Saved path; FALSE on failure.
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)) {
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)) {