5 * Advanced CSS/JS aggregation module.
9 use Drupal\Component\Utility\Crypt;
11 // Core hook implementations.
13 * Implements hook_hook_info().
15 function advagg_hook_info() {
16 // List of hooks that should be inside of *.advagg.inc files.
17 // All advagg hooks except for:
18 // advagg_current_hooks_hash_array_alter
19 // advagg_hooks_implemented_alter
20 // because these 3 hooks are used on most requests.
22 'advagg_aggregate_grouping_alter',
23 'advagg_css_contents_alter',
24 'advagg_js_contents_alter',
25 'advagg_scan_file_alter',
28 foreach ($advagg_hooks as $hook) {
29 $hooks[$hook] = ['group' => 'advagg'];
35 * Implements hook_module_implements_alter().
37 * Move advagg' and various submodule's implementations to last.
39 function advagg_module_implements_alter(&$implementations, $hook) {
40 if ($hook === 'js_alter' && isset($implementations['advagg'])) {
41 // Move advagg and advagg_mod to the bottom, advagg is above advagg_mod.
42 $item = $implementations['advagg'];
43 unset($implementations['advagg']);
44 $implementations['advagg'] = $item;
45 if (isset($implementations['advagg_mod'])) {
46 $item = $implementations['advagg_mod'];
47 unset($implementations['advagg_mod']);
48 $implementations['advagg_mod'] = $item;
51 elseif ($hook === 'css_alter' && isset($implementations['advagg'])) {
52 $item = $implementations['advagg'];
53 unset($implementations['advagg']);
54 $implementations['advagg'] = $item;
55 if (isset($implementations['advagg_mod'])) {
56 $item = $implementations['advagg_mod'];
57 unset($implementations['advagg_mod']);
58 $implementations['advagg_mod'] = $item;
61 if ($hook === 'file_url_alter' && isset($implementations['advagg'])) {
62 $item = $implementations['advagg'];
63 unset($implementations['advagg']);
64 $implementations['advagg'] = $item;
66 if ($hook === 'requirements') {
67 if (isset($implementations['advagg'])) {
68 $item = $implementations['advagg'];
69 unset($implementations['advagg']);
70 $implementations['advagg'] = $item;
72 if (isset($implementations['advagg_cdn'])) {
73 $item = $implementations['advagg_cdn'];
74 unset($implementations['advagg_cdn']);
75 $implementations['advagg_cdn'] = $item;
77 if (isset($implementations['advagg_css_minify'])) {
78 $item = $implementations['advagg_css_minify'];
79 unset($implementations['advagg_css_minify']);
80 $implementations['advagg_css_minify'] = $item;
82 if (isset($implementations['advagg_js_minify'])) {
83 $item = $implementations['advagg_js_minify'];
84 unset($implementations['advagg_js_minify']);
85 $implementations['advagg_js_minify'] = $item;
91 * Implements hook_cron().
93 * This will be ran once a day at most.
95 * @param bool $bypass_time_check
96 * Set to TRUE to skip the 24 hour check.
98 function advagg_cron($bypass_time_check = FALSE) {
99 $state = \Drupal::state();
100 // Execute once a day (24 hours).
101 if (!$bypass_time_check && $state->get('advagg.cron_timestamp', REQUEST_TIME) > (REQUEST_TIME - \Drupal::config('advagg.settings')->get('cron_frequency'))) {
104 $state->set('advagg.cron_timestamp', REQUEST_TIME);
108 'js' => \Drupal::service('asset.js.collection_optimizer')->deleteStale(),
109 'css' => \Drupal::service('asset.css.collection_optimizer')->deleteStale(),
116 * Implements hook_js_alter().
118 function advagg_js_alter(&$js) {
119 // Skip if advagg is disabled.
120 if (!advagg_enabled()) {
124 // Add DNS information for some of the more popular modules.
125 foreach ($js as &$value) {
126 if (!is_string($value['data'])) {
129 // Google Ad Manager.
130 if (strpos($value['data'], '/google_service.') !== FALSE) {
131 if (!empty($value['dns_prefetch']) && is_string($value['dns_prefetch'])) {
132 $temp = $value['dns_prefetch'];
133 unset($value['dns_prefetch']);
134 $value['dns_prefetch'] = [$temp];
136 // Domains in the google_service.js file.
137 $value['dns_prefetch'][] = 'https://csi.gstatic.com';
138 $value['dns_prefetch'][] = 'https://pubads.g.doubleclick.net';
139 $value['dns_prefetch'][] = 'https://partner.googleadservices.com';
140 $value['dns_prefetch'][] = 'https://securepubads.g.doubleclick.net';
142 // Domains in the google_ads.js file.
143 $value['dns_prefetch'][] = 'https://pagead2.googlesyndication.com';
145 // Other domains that usually get hit.
146 $value['dns_prefetch'][] = 'https://cm.g.doubleclick.net';
147 $value['dns_prefetch'][] = 'https://tpc.googlesyndication.com';
151 if (strpos($value['data'], 'GoogleAnalyticsObject') !== FALSE
152 || strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE
154 if (!empty($value['dns_prefetch']) && is_string($value['dns_prefetch'])) {
155 $temp = $value['dns_prefetch'];
156 unset($value['dns_prefetch']);
157 $value['dns_prefetch'] = [$temp];
159 if (strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE) {
160 $value['dns_prefetch'][] = 'https://ssl.google-analytics.com';
162 $value['dns_prefetch'][] = 'https://stats.g.doubleclick.net';
167 // Fix type if it was incorrectly set.
168 if (\Drupal::config('advagg.settings')->get('js_fix_type')) {
169 // Get hostname and base path.
170 $mod_base_url = substr($GLOBALS['base_root'] . $GLOBALS['base_path'], strpos($GLOBALS['base_root'] . $GLOBALS['base_path'], '//') + 2);
171 $mod_base_url_len = strlen($mod_base_url);
173 foreach ($js as &$value) {
174 // Skip if the data is empty or not a string.
175 if (empty($value['data']) || !is_string($value['data'])) {
179 // Default to file if not file or external.
180 if ($value['type'] !== 'file' && $value['type'] !== 'external') {
181 if ($value['type'] === 'settings') {
182 $value['type'] = 'setting';
185 $value['type'] = 'file';
189 // If type is external but doesn't start with http, https, or // change it
191 if ($value['type'] === 'external'
192 && stripos($value['data'], 'http://') !== 0
193 && stripos($value['data'], 'https://') !== 0
194 && stripos($value['data'], '//') !== 0
196 $value['type'] = 'file';
199 // If type is file but it starts with http, https, or // change it to
201 if ($value['type'] === 'file'
202 && (stripos($value['data'], 'http://') === 0
203 || stripos($value['data'], 'https://') === 0
204 || (stripos($value['data'], '//') === 0 && stripos($value['data'], '///') === FALSE))
206 $value['type'] = 'external';
209 // If type is external and starts with http, https, or // but points to
210 // this host change to file, but move it to the top of the aggregation
211 // stack as long as js_preserve_external is not set.
212 if (\Drupal::config('advagg.settings')->get('js_preserve_external') === FALSE) {
213 if ($value['type'] === 'external'
214 && stripos($value['data'], $mod_base_url) !== FALSE
215 && (stripos($value['data'], 'http://') === 0
216 || stripos($value['data'], 'https://') === 0
217 || stripos($value['data'], '//') === 0
220 $value['type'] = 'file';
221 $value['group'] = JS_LIBRARY;
222 $value['every_page'] = TRUE;
223 $value['weight'] = -40000;
224 $value['data'] = substr($value['data'], stripos($value['data'], $mod_base_url) + $mod_base_url_len);
233 * Implements hook_css_alter().
235 function advagg_css_alter(&$css) {
236 if (!advagg_enabled()) {
239 if (\Drupal::config('advagg.settings')->get('css.fix_type')) {
240 // Fix type if it was incorrectly set.
241 foreach ($css as &$value) {
242 if (empty($value['data']) || !is_string($value['data'])) {
246 // Default to file if not set.
247 if ($value['type'] !== 'file' && $value['type'] !== 'external') {
248 $value['type'] = 'file';
251 // If type is external but doesn't start with http, https, or // change it
253 if ($value['type'] === 'external'
254 && stripos($value['data'], 'http://') !== 0
255 && stripos($value['data'], 'https://') !== 0
256 && stripos($value['data'], '//') !== 0
258 $value['type'] = 'file';
261 // If type is file but it starts with http, https, or // change it to
263 if ($value['type'] === 'file'
264 && (stripos($value['data'], 'http://') === 0
265 || stripos($value['data'], 'https://') === 0
266 || (stripos($value['data'], '//') === 0
267 && stripos($value['data'], '///') === FALSE
271 $value['type'] = 'external';
279 * Implements hook_form_FORM_ID_alter().
281 * Give advice on how to temporarily disable css/js aggregation.
283 function advagg_form_system_performance_settings_alter(&$form, &$form_state) {
284 $msg = t('NOTE: If you wish to bypass aggregation for a set amount of time, you can go to the <a href="@operations">AdvAgg operations</a> page and press the "aggregation bypass cookie" button.', [
285 '@operations' => Url::fromRoute('advagg.operations')->toString(),
288 if (\Drupal::currentUser()->hasPermission('bypass advanced aggregation')) {
289 $msg .= t('You can also selectively bypass aggregation by adding <code>@code</code> to the URL of any page.', [
290 '@code' => '?advagg=0',
294 $msg .= t('You do not have the <a href="@permission">bypass advanced aggregation permission</a> so adding <code>@code</code> to the URL will not work at this time for you; either grant this permission to your user role or use the bypass cookie if you wish to selectively bypass aggregation.', [
295 '@permission' => Url::fromRoute('user.admin_permissions')->toString(),
296 '@code' => '?advagg=0',
300 $form['bandwidth_optimization']['advagg_note'] = [
306 * Returns TRUE if the CSS is being loaded via JavaScript.
309 * TRUE if CSS loaded via JS. FALSE if not.
311 function advagg_css_in_js() {
312 if (\Drupal::moduleHandler()->moduleExists('advagg_mod')
313 && \Drupal::config('advagg_mod.settings')->get('css_defer')
317 return \Drupal::config('advagg.settings')->get('advagg_css_in_js');
322 * Function used to see if aggregation is enabled.
325 * The value of the advagg_enabled variable.
327 function advagg_enabled() {
328 $init = &drupal_static(__FUNCTION__);
331 return $init['advagg'];
334 $advagg_config = \Drupal::config('advagg.settings');
335 $user = \Drupal::currentUser();
336 $init['advagg'] = $advagg_config->get('enabled');
338 // Disable AdvAgg if maintenance mode is defined.
339 if (defined('MAINTENANCE_MODE')) {
340 $init['advagg'] = FALSE;
343 // Allow for AdvAgg to be enabled/disabled per request.
344 if (isset($_GET['advagg']) && $user->hasPermission('bypass advanced aggregation')) {
345 if ($_GET['advagg'] == 1) {
346 $init['advagg'] = TRUE;
349 $init['advagg'] = FALSE;
353 // Do not use AdvAgg if the disable cookie is set.
354 $cookie_name = 'AdvAggDisabled';
355 $key = Crypt::hashBase64(\Drupal::service('private_key')->get());
356 if (!empty($_COOKIE[$cookie_name]) && $_COOKIE[$cookie_name] == $key) {
357 $init['advagg'] = FALSE;
359 // Let the user know that the AdvAgg bypass cookie is currently set.
361 if (!isset($msg_set) && $advagg_config->get('show_bypass_cookie_message')) {
363 if (\Drupal::currentUser()->hasPermission('administer site configuration')) {
364 drupal_set_message(t('The AdvAgg bypass cookie is currently enabled. Turn it off by going to the <a href="@advagg_operations">AdvAgg Operations</a> page and clicking the <em>Toggle the "aggregation bypass cookie" for this browser</em> button.', [
365 '@advagg_operations' => Url::fromRoute('advagg.operations', [], ['fragment' => 'edit-bypass'])->toString(),
369 drupal_set_message(t('The AdvAgg bypass cookie is currently enabled. Turn it off by <a href="@login">logging in</a> with a user with the "administer site configuration" permissions and going to the AdvAgg Operations page (located at @advagg_operations) and clicking the <em>Toggle the "aggregation bypass cookie" for this browser</em> button.', [
370 '@login' => '/user/login',
371 '@advagg_operations' => Url::fromRoute('advagg.operations')->toString(),
377 // Enable debugging if requested.
378 if (isset($_GET['advagg-debug'])
379 && $_GET['advagg-debug'] == 1
380 && $user->hasPermission('bypass advanced aggregation')
383 $config['advagg.settings']['debug'] = TRUE;
386 return $init['advagg'];
390 * Get an array of all hooks and settings that affect aggregated files contents.
393 * ['variables' => [], 'hooks' => []]
395 function advagg_current_hooks_hash_array() {
396 $aggregate_settings = &drupal_static(__FUNCTION__);
397 if (isset($aggregate_settings)) {
398 return $aggregate_settings;
401 $config = \Drupal::config('advagg.settings');
403 // Put all enabled hooks and settings into a big array.
404 $aggregate_settings = [
405 'variables' => ['advagg' => $config->get()],
406 'hooks' => advagg_hooks_implemented(FALSE),
409 // Add in language if locale is enabled.
410 if (\Drupal::moduleHandler()->moduleExists('locale')) {
411 $aggregate_settings['variables']['language'] = isset(\Drupal::languageManager()->getCurrentLanguage()->language) ? \Drupal::languageManager()->getCurrentLanguage()->language : '';
414 // Add the base url if so desired to.
415 if ($config->get('include_base_url')) {
416 $aggregate_settings['variables']['base_url'] = $GLOBALS['base_url'];
419 // Allow other modules to add in their own settings and hooks.
420 // Call hook_advagg_current_hooks_hash_array_alter().
421 \Drupal::moduleHandler()->alter('advagg_current_hooks_hash_array', $aggregate_settings);
423 return $aggregate_settings;
427 * Get the serialization function.
430 * The serializer. Defaults to json_encode.
432 function advagg_get_serializer() {
433 $serialize_function = \Drupal::config('advagg.settings')->get('serializer');
434 if (!is_string($serialize_function) || !is_callable($serialize_function)) {
435 $serialize_function = 'json_encode';
437 return $serialize_function;
441 * Get the hash of all hooks and settings that affect aggregated files contents.
446 function advagg_get_current_hooks_hash() {
447 $current_hash = &drupal_static(__FUNCTION__);
449 if (!isset($current_hash)) {
450 // Get all advagg hooks and variables in use.
451 $aggregate_settings = advagg_current_hooks_hash_array();
453 // Generate the hash.
454 $serialize_function = advagg_get_serializer();
455 $current_hash = Crypt::hashBase64($serialize_function($aggregate_settings));
458 return $current_hash;
462 * Get back what hooks are implemented.
465 * If TRUE get all hooks related to css/js files.
466 * if FALSE get only the subset of hooks that alter the filename/contents.
469 * List of hooks and what modules have implemented them.
471 function advagg_hooks_implemented($all = TRUE) {
472 $hooks = &drupal_static(__FUNCTION__);
476 $module_handler = \Drupal::moduleHandler();
480 'advagg_aggregate_grouping_alter' => [],
481 'advagg_css_contents_alter' => [],
482 'advagg_js_contents_alter' => [],
483 'advagg_current_hooks_hash_array_alter' => [],
484 'advagg_hooks_implemented' => [],
492 // Call hook_advagg_hooks_implemented_alter().
493 $module_handler->alter('advagg_hooks_implemented', $hooks, $all);
495 // Cache module_implements as this will load up .inc files.
496 $serialize_function = advagg_get_serializer();
497 $cid = 'advagg_hooks_implemented:' . (int) $all . ':' . Crypt::hashBase64($serialize_function($hooks));
498 $cache = \Drupal::cache('bootstrap')->get($cid);
499 if (!empty($cache->data)) {
500 $hooks = $cache->data;
503 foreach ($hooks as $hook => $values) {
504 $hooks[$hook] = $module_handler->getImplementations($hook);
506 // Also check themes as drupal_alter() allows for themes to alter things.
507 $theme_keys = \Drupal::service('theme_handler')->listInfo();
508 if (!empty($theme_keys)) {
509 foreach ($theme_keys as $theme_key => $info) {
510 $function = $theme_key . '_' . $hook;
511 if (function_exists($function)) {
512 $hooks[$hook][] = $info['name'];
517 \Drupal::cache('bootstrap')->set($cid, $hooks, REQUEST_TIME + 600);
523 * Given a uri, get the relative_path.
526 * The uri for the stream wrapper.
529 * The relative path of the uri.
531 * @see https://www.drupal.org/node/837794#comment-9124435
533 function advagg_get_relative_path($uri) {
534 $wrapper = \Drupal::service("stream_wrapper_manager")->getViaUri($uri);
535 if ($wrapper instanceof DrupalLocalStreamWrapper) {
536 $relative_path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri);
539 $relative_path = parse_url(file_create_url($uri), PHP_URL_PATH);
540 if (substr($relative_path, 0, strlen($GLOBALS['base_path'])) == $GLOBALS['base_path']) {
541 $relative_path = substr($relative_path, strlen($GLOBALS['base_path']));
544 return $relative_path;
548 * Return the global_counter variable.
553 function advagg_get_global_counter() {
554 $counter = &drupal_static(__FUNCTION__);
556 $counter = \Drupal::config('advagg.settings')->get('global_counter');
557 if ($counter === NULL) {
565 * Stable sort for CSS and JS items.
567 * Preserves the order of items with equal sort criteria.
569 * The function will sort by:
570 * - $item['group'], integer, ascending
571 * - $item['weight'], integer, ascending
573 * @param array &$items
574 * Array of JS or CSS items, as in hook_alter_js() and hook_alter_css().
575 * The array keys can be integers or strings. The items themselves are arrays.
577 * @see hook_alter_js()
578 * @see hook_alter_css()
580 function advagg_drupal_sort_css_js_stable(array &$items) {
582 foreach ($items as $key => $item) {
583 // Weight cast to string to preserve float.
584 $weight = (string) $item['weight'];
585 $nested[$item['group']][$weight][$key] = $item;
587 // First order by group, so that, for example, all items in the CSS_SYSTEM
588 // group appear before items in the CSS_DEFAULT group, which appear before
589 // all items in the CSS_THEME group. Modules may create additional groups by
590 // defining their own constants.
592 // Sort group; then iterate over it.
594 foreach ($nested as &$group_items) {
595 // Order by weight and iterate over it.
597 foreach ($group_items as &$weight_items) {
598 foreach ($weight_items as $key => &$item) {
599 $sorted[$key] = $item;
603 unset($weight_items);
610 * Converts absolute paths to be self references.
612 * @param string $path
618 function advagg_convert_abs_to_rel($path) {
619 if (strpos($path, $GLOBALS['base_url']) === 0) {
620 $base_url = $GLOBALS['base_url'];
621 // Add a slash if none is found.
622 if (stripos(strrev($base_url), '/') !== 0) {
625 $path = str_replace($base_url, $GLOBALS['base_path'], $path);
631 * Converts absolute paths to be protocol relative paths.
633 * @param string $path
639 function advagg_path_convert_protocol_relative($path) {
640 if (strpos($path, 'https://') === 0) {
641 $path = substr($path, 6);
643 elseif (strpos($path, 'http://') === 0) {
644 $path = substr($path, 5);
650 * Convert http:// to https://.
652 * @param string $path
658 function advagg_path_convert_force_https($path) {
659 if (strpos($path, 'http://') === 0) {
660 $path = 'https://' . substr($path, 7);
666 * Convert the saved advagg cache level to a time interval.
674 function advagg_get_cache_time($level = 0) {