Including security review as a submodule - with patched for Yaffs.
[yaffs-website] / web / modules / contrib / advagg / advagg.module
1 <?php
2
3 /**
4  * @file
5  * Advanced CSS/JS aggregation module.
6  */
7
8 use Drupal\Core\Url;
9 use Drupal\Component\Utility\Crypt;
10
11 // Core hook implementations.
12 /**
13  * Implements hook_hook_info().
14  */
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.
21   $advagg_hooks = [
22     'advagg_aggregate_grouping_alter',
23     'advagg_css_contents_alter',
24     'advagg_js_contents_alter',
25     'advagg_scan_file_alter',
26   ];
27   $hooks = [];
28   foreach ($advagg_hooks as $hook) {
29     $hooks[$hook] = ['group' => 'advagg'];
30   }
31   return $hooks;
32 }
33
34 /**
35  * Implements hook_module_implements_alter().
36  *
37  * Move advagg' and various submodule's implementations to last.
38  */
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;
49     }
50   }
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;
59     }
60   }
61   if ($hook === 'file_url_alter' && isset($implementations['advagg'])) {
62     $item = $implementations['advagg'];
63     unset($implementations['advagg']);
64     $implementations['advagg'] = $item;
65   }
66   if ($hook === 'requirements') {
67     if (isset($implementations['advagg'])) {
68       $item = $implementations['advagg'];
69       unset($implementations['advagg']);
70       $implementations['advagg'] = $item;
71     }
72     if (isset($implementations['advagg_cdn'])) {
73       $item = $implementations['advagg_cdn'];
74       unset($implementations['advagg_cdn']);
75       $implementations['advagg_cdn'] = $item;
76     }
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;
81     }
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;
86     }
87   }
88 }
89
90 /**
91  * Implements hook_cron().
92  *
93  * This will be ran once a day at most.
94  *
95  * @param bool $bypass_time_check
96  *   Set to TRUE to skip the 24 hour check.
97  */
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'))) {
102     return [];
103   }
104   $state->set('advagg.cron_timestamp', REQUEST_TIME);
105
106   $return = [];
107   $return['stale'] = [
108     'js' => \Drupal::service('asset.js.collection_optimizer')->deleteStale(),
109     'css' => \Drupal::service('asset.css.collection_optimizer')->deleteStale(),
110   ];
111
112   return $return;
113 }
114
115 /**
116  * Implements hook_js_alter().
117  */
118 function advagg_js_alter(&$js) {
119   // Skip if advagg is disabled.
120   if (!advagg_enabled()) {
121     return;
122   }
123
124   // Add DNS information for some of the more popular modules.
125   foreach ($js as &$value) {
126     if (!is_string($value['data'])) {
127       continue;
128     }
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];
135       }
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';
141
142       // Domains in the google_ads.js file.
143       $value['dns_prefetch'][] = 'https://pagead2.googlesyndication.com';
144
145       // Other domains that usually get hit.
146       $value['dns_prefetch'][] = 'https://cm.g.doubleclick.net';
147       $value['dns_prefetch'][] = 'https://tpc.googlesyndication.com';
148     }
149
150     // Google Analytics.
151     if (strpos($value['data'], 'GoogleAnalyticsObject') !== FALSE
152       || strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE
153     ) {
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];
158       }
159       if (strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE) {
160         $value['dns_prefetch'][] = 'https://ssl.google-analytics.com';
161       }
162       $value['dns_prefetch'][] = 'https://stats.g.doubleclick.net';
163     }
164   }
165   unset($value);
166
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);
172
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'])) {
176         continue;
177       }
178
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';
183         }
184         else {
185           $value['type'] = 'file';
186         }
187       }
188
189       // If type is external but doesn't start with http, https, or // change it
190       // to file.
191       if ($value['type'] === 'external'
192         && stripos($value['data'], 'http://') !== 0
193         && stripos($value['data'], 'https://') !== 0
194         && stripos($value['data'], '//') !== 0
195         ) {
196         $value['type'] = 'file';
197       }
198
199       // If type is file but it starts with http, https, or // change it to
200       // external.
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))
205         ) {
206         $value['type'] = 'external';
207       }
208
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
218           )
219         ) {
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);
225         }
226       }
227     }
228     unset($value);
229   }
230 }
231
232 /**
233  * Implements hook_css_alter().
234  */
235 function advagg_css_alter(&$css) {
236   if (!advagg_enabled()) {
237     return;
238   }
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'])) {
243         continue;
244       }
245
246       // Default to file if not set.
247       if ($value['type'] !== 'file' && $value['type'] !== 'external') {
248         $value['type'] = 'file';
249       }
250
251       // If type is external but doesn't start with http, https, or // change it
252       // to file.
253       if ($value['type'] === 'external'
254         && stripos($value['data'], 'http://') !== 0
255         && stripos($value['data'], 'https://') !== 0
256         && stripos($value['data'], '//') !== 0
257         ) {
258         $value['type'] = 'file';
259       }
260
261       // If type is file but it starts with http, https, or // change it to
262       // external.
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
268         )
269         )
270       ) {
271         $value['type'] = 'external';
272       }
273     }
274     unset($value);
275   }
276 }
277
278 /**
279  * Implements hook_form_FORM_ID_alter().
280  *
281  * Give advice on how to temporarily disable css/js aggregation.
282  */
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(),
286   ]);
287
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',
291     ]);
292   }
293   else {
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',
297     ]);
298   }
299
300   $form['bandwidth_optimization']['advagg_note'] = [
301     '#markup' => $msg,
302   ];
303 }
304
305 /**
306  * Returns TRUE if the CSS is being loaded via JavaScript.
307  *
308  * @return bool
309  *   TRUE if CSS loaded via JS. FALSE if not.
310  */
311 function advagg_css_in_js() {
312   if (\Drupal::moduleHandler()->moduleExists('advagg_mod')
313       && \Drupal::config('advagg_mod.settings')->get('css_defer')
314     ) {
315     return TRUE;
316   }
317   return \Drupal::config('advagg.settings')->get('advagg_css_in_js');
318 }
319
320 // Helper functions.
321 /**
322  * Function used to see if aggregation is enabled.
323  *
324  * @return bool
325  *   The value of the advagg_enabled variable.
326  */
327 function advagg_enabled() {
328   $init = &drupal_static(__FUNCTION__);
329
330   if (!empty($init)) {
331     return $init['advagg'];
332   }
333
334   $advagg_config = \Drupal::config('advagg.settings');
335   $user = \Drupal::currentUser();
336   $init['advagg'] = $advagg_config->get('enabled');
337
338   // Disable AdvAgg if maintenance mode is defined.
339   if (defined('MAINTENANCE_MODE')) {
340     $init['advagg'] = FALSE;
341     return FALSE;
342   }
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;
347     }
348     else {
349       $init['advagg'] = FALSE;
350     }
351   }
352
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;
358
359     // Let the user know that the AdvAgg bypass cookie is currently set.
360     static $msg_set;
361     if (!isset($msg_set) && $advagg_config->get('show_bypass_cookie_message')) {
362       $msg_set = TRUE;
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(),
366         ]));
367       }
368       else {
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(),
372         ]));
373       }
374     }
375   }
376
377   // Enable debugging if requested.
378   if (isset($_GET['advagg-debug'])
379     && $_GET['advagg-debug'] == 1
380     && $user->hasPermission('bypass advanced aggregation')
381     ) {
382     global $config;
383     $config['advagg.settings']['debug'] = TRUE;
384   }
385
386   return $init['advagg'];
387 }
388
389 /**
390  * Get an array of all hooks and settings that affect aggregated files contents.
391  *
392  * @return array
393  *   ['variables' => [], 'hooks' => []]
394  */
395 function advagg_current_hooks_hash_array() {
396   $aggregate_settings = &drupal_static(__FUNCTION__);
397   if (isset($aggregate_settings)) {
398     return $aggregate_settings;
399   }
400
401   $config = \Drupal::config('advagg.settings');
402
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),
407   ];
408
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 : '';
412   }
413
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'];
417   }
418
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);
422
423   return $aggregate_settings;
424 }
425
426 /**
427  * Get the serialization function.
428  *
429  * @return string
430  *   The serializer. Defaults to json_encode.
431  */
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';
436   }
437   return $serialize_function;
438 }
439
440 /**
441  * Get the hash of all hooks and settings that affect aggregated files contents.
442  *
443  * @return string
444  *   hash value.
445  */
446 function advagg_get_current_hooks_hash() {
447   $current_hash = &drupal_static(__FUNCTION__);
448
449   if (!isset($current_hash)) {
450     // Get all advagg hooks and variables in use.
451     $aggregate_settings = advagg_current_hooks_hash_array();
452
453     // Generate the hash.
454     $serialize_function = advagg_get_serializer();
455     $current_hash = Crypt::hashBase64($serialize_function($aggregate_settings));
456   }
457
458   return $current_hash;
459 }
460
461 /**
462  * Get back what hooks are implemented.
463  *
464  * @param bool $all
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.
467  *
468  * @return array
469  *   List of hooks and what modules have implemented them.
470  */
471 function advagg_hooks_implemented($all = TRUE) {
472   $hooks = &drupal_static(__FUNCTION__);
473   if ($hooks) {
474     return $hooks;
475   }
476   $module_handler = \Drupal::moduleHandler();
477
478   // Get hooks in use.
479   $hooks = [
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' => [],
485   ];
486   if ($all) {
487     $hooks += [
488       'js_alter' => [],
489       'css_alter' => [],
490     ];
491   }
492   // Call hook_advagg_hooks_implemented_alter().
493   $module_handler->alter('advagg_hooks_implemented', $hooks, $all);
494
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;
501   }
502   else {
503     foreach ($hooks as $hook => $values) {
504       $hooks[$hook] = $module_handler->getImplementations($hook);
505
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'];
513           }
514         }
515       }
516     }
517     \Drupal::cache('bootstrap')->set($cid, $hooks, REQUEST_TIME + 600);
518   }
519   return $hooks;
520 }
521
522 /**
523  * Given a uri, get the relative_path.
524  *
525  * @param string $uri
526  *   The uri for the stream wrapper.
527  *
528  * @return string
529  *   The relative path of the uri.
530  *
531  * @see https://www.drupal.org/node/837794#comment-9124435
532  */
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);
537   }
538   else {
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']));
542     }
543   }
544   return $relative_path;
545 }
546
547 /**
548  * Return the global_counter variable.
549  *
550  * @return int
551  *   Int value.
552  */
553 function advagg_get_global_counter() {
554   $counter = &drupal_static(__FUNCTION__);
555   if (!$counter) {
556     $counter = \Drupal::config('advagg.settings')->get('global_counter');
557     if ($counter === NULL) {
558       $counter = 0;
559     }
560   }
561   return $counter;
562 }
563
564 /**
565  * Stable sort for CSS and JS items.
566  *
567  * Preserves the order of items with equal sort criteria.
568  *
569  * The function will sort by:
570  * - $item['group'],      integer, ascending
571  * - $item['weight'],     integer, ascending
572  *
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.
576  *
577  * @see hook_alter_js()
578  * @see hook_alter_css()
579  */
580 function advagg_drupal_sort_css_js_stable(array &$items) {
581   $nested = [];
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;
586   }
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.
591   $sorted = [];
592   // Sort group; then iterate over it.
593   ksort($nested);
594   foreach ($nested as &$group_items) {
595     // Order by weight and iterate over it.
596     ksort($group_items);
597     foreach ($group_items as &$weight_items) {
598       foreach ($weight_items as $key => &$item) {
599         $sorted[$key] = $item;
600       }
601       unset($item);
602     }
603     unset($weight_items);
604   }
605   unset($group_items);
606   $items = $sorted;
607 }
608
609 /**
610  * Converts absolute paths to be self references.
611  *
612  * @param string $path
613  *   Path to check.
614  *
615  * @return string
616  *   The path.
617  */
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) {
623       $base_url .= '/';
624     }
625     $path = str_replace($base_url, $GLOBALS['base_path'], $path);
626   }
627   return $path;
628 }
629
630 /**
631  * Converts absolute paths to be protocol relative paths.
632  *
633  * @param string $path
634  *   Path to check.
635  *
636  * @return string
637  *   The path.
638  */
639 function advagg_path_convert_protocol_relative($path) {
640   if (strpos($path, 'https://') === 0) {
641     $path = substr($path, 6);
642   }
643   elseif (strpos($path, 'http://') === 0) {
644     $path = substr($path, 5);
645   }
646   return $path;
647 }
648
649 /**
650  * Convert http:// to https://.
651  *
652  * @param string $path
653  *   Path to check.
654  *
655  * @return string
656  *   The path.
657  */
658 function advagg_path_convert_force_https($path) {
659   if (strpos($path, 'http://') === 0) {
660     $path = 'https://' . substr($path, 7);
661   }
662   return $path;
663 }
664
665 /**
666  * Convert the saved advagg cache level to a time interval.
667  *
668  * @param int $level
669  *   The cache level.
670  *
671  * @return int
672  *   The time interval.
673  */
674 function advagg_get_cache_time($level = 0) {
675   switch ($level) {
676     case 1:
677     case 3:
678       return 86400;
679
680     case 5:
681       return 604800;
682
683     case 0:
684     default:
685       return 0;
686   }
687 }