core_library.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <?php
  2. /**
  3. * @file
  4. * Core Library module.
  5. */
  6. /**
  7. * Aggregate file on all pages.
  8. */
  9. define('LIBRARY_AGGREGATE_ALL', 1);
  10. /**
  11. * Aggregate file on a per-path basis.
  12. */
  13. define('LIBRARY_AGGREGATE_PATH', 2);
  14. /**
  15. * Let core follow its own rules for this file aggregation.
  16. */
  17. define('LIBRARY_AGGREGATE_DEFAULT', 3);
  18. /**
  19. * Site admin defined aggregation rules.
  20. */
  21. define('LIBRARY_MODE_MANUAL', 1);
  22. /**
  23. * Learn and aggregate all files in anonymous browsing.
  24. */
  25. define('LIBRARY_MODE_LEARNING_ANONYMOUS', 2);
  26. /**
  27. * Learn and aggregate all files in browsing, admin pages excluded.
  28. */
  29. define('LIBRARY_MODE_LEARNING_NO_ADMIN', 3);
  30. /**
  31. * Learn and aggregate all files.
  32. */
  33. define('LIBRARY_MODE_LEARNING_ALL', 4);
  34. /**
  35. * Do not, ever, let the core aggregate files, and use the latest one whatever
  36. * happens. This means all new files will be ignored.
  37. */
  38. define('LIBRARY_MODE_BYPASS', 5);
  39. /**
  40. * Group CSS in 2 groups, all around in one side, theme specifics on the other.
  41. */
  42. define('LIBRARY_CSS_GROUP_DUAL', 1);
  43. /**
  44. * Implements hook_menu().
  45. */
  46. function core_library_menu() {
  47. $items = array();
  48. $items['admin/config/development/library'] = array(
  49. 'title' => "Library",
  50. 'description' => "Manage external JS and CSS libraries and allows to set different aggregation rules.",
  51. 'page callback' => 'drupal_get_form',
  52. 'page arguments' => array('core_library_admin_settings'),
  53. 'access arguments' => array('administer site configuration'),
  54. 'type' => MENU_NORMAL_ITEM,
  55. 'file' => 'core_library.admin.inc',
  56. );
  57. return $items;
  58. }
  59. /**
  60. * Get JSMin path.
  61. *
  62. * FIXME: Sorry, this is hardcoded right now, will change in the future.
  63. *
  64. * @return string
  65. * Found library, NULL if not found.
  66. */
  67. function core_library_js_minify_library_path() {
  68. if ($path = variable_get('core_library_jsmin_path', NULL)) {
  69. return $path;
  70. }
  71. else {
  72. return 'sites/all/libraries/jsmin/jsmin.php';
  73. }
  74. }
  75. /**
  76. * Minify given JS if not already done.
  77. */
  78. function core_library_js_minify(&$options) {
  79. static $minified, $included = FALSE;
  80. if (!isset($minified)) {
  81. $minified = variable_get('core_library_js_minified', array());
  82. }
  83. // Code readability, my friend.
  84. $source = $options['data'];
  85. // We should allow exclusion here, user configured.
  86. if (!array_key_exists($source, $minified)) {
  87. // Do not compress already compressed files.
  88. if (!strpos($source, '.min.js')) {
  89. // Attempt to use jsmin library if present.
  90. if (!$included) {
  91. $libpath = core_library_js_minify_library_path();
  92. require_once DRUPAL_ROOT . '/' . $libpath;
  93. $included = TRUE;
  94. }
  95. // Why does this functions is not wrapped in file.inc? It should, since we
  96. // are supposed to manipulate every file using stream wrappers.
  97. $buf = file_get_contents($source);
  98. $buf = JSMin::minify($buf);
  99. // Compute new filename.
  100. $parts = explode('/', $source);
  101. $filename = end($parts);
  102. $file_uri = 'public://static/' . str_replace('.js', '.min.js', $filename);
  103. // Save it.
  104. if ($file_uri = file_unmanaged_save_data($buf, $file_uri, FILE_EXISTS_REPLACE)) {
  105. if ($wrapper = file_stream_wrapper_get_instance_by_uri($file_uri)) {
  106. // Protected getTarget() function emulation.
  107. list($scheme, $target) = explode('://', $file_uri, 2);
  108. $target = trim($target, '\/');
  109. $file_uri = $wrapper->getDirectoryPath() . '/' . $target;
  110. }
  111. watchdog('core_library', "File '@src' minified successfully to '@dest'.", array(
  112. '@src' => $source,
  113. '@dest' => $file_uri,
  114. ));
  115. // Save is ok, put the new filename in the $options array
  116. $options['data'] = $file_uri;
  117. // Tell the system we minified a new file.
  118. $minified[$source] = $file_uri;
  119. variable_set('core_library_js_minified', $minified);
  120. }
  121. }
  122. }
  123. else {
  124. // We have already minified this file in the past, let's reuse the cached
  125. // result path for alteration.
  126. $options['data'] = $minified[$source];
  127. }
  128. }
  129. /**
  130. * Modify the given JavaScript description to enforce it to go into the main
  131. * libraries group, so the core will create only one big aggregated file.
  132. */
  133. function core_library_defaults_js(&$data) {
  134. // Custom override.
  135. static $minify_enabled = NULL, $defaults = array(
  136. 'every_page' => TRUE,
  137. 'preprocess' => TRUE,
  138. 'cache' => TRUE,
  139. 'defer' => FALSE,
  140. 'type' => 'file',
  141. 'scope' => 'header',
  142. );
  143. // Ignore 'inline' or 'external' content.
  144. // Not set type means we are getting this entry from our own cache.
  145. if (isset($data['type']) && 'file' !== $data['type']) {
  146. return;
  147. }
  148. // Prepare some variables at first hit.
  149. if (!isset($minify_enabled)) {
  150. $minify_enabled = variable_get('core_library_js_minify', FALSE);
  151. }
  152. // Set default weight for every file. If we don't drupal_get_css() will
  153. // fail at uasort() time on custom added JS files.
  154. if (!array_key_exists('weight', $data)) {
  155. $data['weight'] = 0;
  156. }
  157. // First thing to do is to force the current group to be libraries one.
  158. if (!array_key_exists('group', $data) || $data['group'] != JS_LIBRARY) {
  159. $data['group'] = JS_LIBRARY;
  160. // Once we done that, we have to alter weight to ensure no outsider will
  161. // take precedence over real libraries.
  162. $data['weight'] += 1000;
  163. }
  164. // Override data.
  165. foreach ($defaults as $name => $value) {
  166. $data[$name] = $value;
  167. }
  168. // Minify, if enabled.
  169. if ($minify_enabled) {
  170. core_library_js_minify($data);
  171. }
  172. }
  173. /**
  174. * Modify the given CSS description to enforce it to go into the main
  175. * system group, so the core will create only one big aggregated file.
  176. */
  177. function core_library_defaults_css(&$data) {
  178. // Defaults won't handle the 'media' key. Modules and libraries do specify
  179. // their own value for this (we only set the 'all' value when no value is
  180. // given).
  181. static $please_group_css, $defaults = array(
  182. 'type' => 'file',
  183. 'every_page' => TRUE,
  184. 'preprocess' => TRUE,
  185. );
  186. // Ignore 'inline' or 'external' content.
  187. // Not set type means we are getting this entry from our own cache.
  188. if (isset($data['type']) && 'file' !== $data['type']) {
  189. return;
  190. }
  191. // Statically initialize variables used for the rest of the algorithm.
  192. if (!isset($please_group_css)) {
  193. $css_grouping_mode = variable_get('core_library_css_group', FALSE);
  194. $please_group_css = (bool) $css_grouping_mode;
  195. }
  196. // Assign a default group for files that does not have any. This can happen
  197. // within our own execution workflow because we override a lot of stuff from
  198. // core.
  199. if (!array_key_exists('group', $data)) {
  200. $data['group'] = CSS_DEFAULT;
  201. }
  202. // In some case, browsers are not specified, which make the aggregation
  203. // algorithm to have a specific group for those. We don't want that, no
  204. // browser specified means all browsers, like any other files.
  205. // This is a weird behavior since it happens only for some core files
  206. // included with the drupal_add_css() $every_page parameter set to TRUE,
  207. // see system_init() implementation for example.
  208. if (empty($data['browsers'])) {
  209. $data['browsers'] = array(
  210. 'IE' => TRUE,
  211. '!IE' => TRUE,
  212. );
  213. }
  214. // Same bug as above, for media.
  215. if (empty($data['media'])) {
  216. $data['media'] = 'all';
  217. }
  218. // Set default weight for every file. If we don't drupal_get_css() will
  219. // fail at uasort() time on custom added JS files.
  220. if (!array_key_exists('weight', $data)) {
  221. $data['weight'] = 0;
  222. }
  223. // First thing to do is to force the current group to be libraries one.
  224. if ($please_group_css) {
  225. if ($data['group'] != CSS_THEME) {
  226. $data['group'] = CSS_SYSTEM;
  227. // Once we done that, we have to alter weight to ensure no outsider will
  228. // take precedence over real libraries.
  229. $data['weight'] += 1000;
  230. }
  231. }
  232. // Override data.
  233. foreach ($defaults as $name => $value) {
  234. $data[$name] = $value;
  235. }
  236. }
  237. function core_library_bypass_mode() {
  238. // Compat fix with overlay module.
  239. // FIXME: A better solution should be developped here.
  240. if (module_exists('overlay') && user_access('access-overlay')) {
  241. return NULL;
  242. }
  243. return variable_get('core_library_bypass', NULL);
  244. }
  245. /**
  246. * Implements hook_element_info_alter().
  247. *
  248. * Depending on the actual aggregation mode we are, we will change the current
  249. * JS / CSS aggregation functions to ensure Drupal won't ever, ever attempt any
  250. * file_exists() or hash compute while we are sure our CSS and JS files are
  251. * stable and cover the full site.
  252. */
  253. function core_library_element_info_alter(&$type) {
  254. // Change the 'style' element type rendering if needed.
  255. if (core_library_bypass_mode() == LIBRARY_MODE_BYPASS) {
  256. // Force our file to be included if needed.
  257. require_once drupal_get_path('module', 'core_library') . '/core_library.bypass.inc';
  258. $type['styles'] = array(
  259. '#items' => array(),
  260. '#pre_render' => array('core_library_element_style_pre_render'),
  261. );
  262. }
  263. }
  264. /**
  265. * Implements hook_init().
  266. */
  267. function core_library_init() {
  268. // We will aggregate libraries files only if are not in learning mode.
  269. // Learning mode will find them as classic files, without having to known
  270. // if they are part of a library or not.
  271. if (core_library_bypass_mode() == LIBRARY_MODE_MANUAL) {
  272. require_once drupal_get_path('module', 'core_library') . '/core_library.manual.inc';
  273. core_library_manual_init();
  274. }
  275. }
  276. /**
  277. * Single point where all exclusion rules happen.
  278. *
  279. * @param string $type
  280. * Library type (e.g. 'js' or 'css').
  281. * @param string $file
  282. * Original library key in libraries array.
  283. * @param array $options
  284. * Original library options.
  285. *
  286. * @return bool
  287. * TRUE if should not preprocess this file ourselves.
  288. */
  289. function _core_library_must_exclude($type, $file, $options) {
  290. static $respectful_preprocess;
  291. if (!isset($js_respect_preprocess)) {
  292. $respectful_preprocess = variable_get('core_library_respectful_preprocess', TRUE);
  293. }
  294. if ($respectful_preprocess && isset($options['preprocess']) && !$options['preprocess']) {
  295. return TRUE;
  296. }
  297. return FALSE;
  298. }
  299. /**
  300. * Merge the given array using the given type.
  301. *
  302. * This is the magic function where all learning happens.
  303. */
  304. function _core_library_merge(&$array, $type) {
  305. global $theme_key;
  306. // Following static variables are pure optimization.
  307. static $do_learn, $theme_path, $theme_len;
  308. // Set the current learning mode.
  309. if (!isset($do_learn)) {
  310. switch (core_library_bypass_mode()) {
  311. case LIBRARY_MODE_LEARNING_ANONYMOUS:
  312. $do_learn = user_is_anonymous();
  313. break;
  314. case LIBRARY_MODE_LEARNING_NO_ADMIN:
  315. $do_learn = !drupal_match_path($_GET['q'], "admin\nadmin/*");
  316. break;
  317. case LIBRARY_MODE_LEARNING_ALL:
  318. $do_learn = TRUE;
  319. break;
  320. case LIBRARY_MODE_BYPASS:
  321. // In bypass mode, we still need libraries to be ordered in order for
  322. // our custom style pre_render function to build correct CSS files.
  323. $do_learn = FALSE;
  324. break;;
  325. default:
  326. // We should never get here, but always keep a fallback.
  327. $exclude_path = "*";
  328. break;
  329. }
  330. // For later use.
  331. $theme_path = drupal_get_path('theme', $theme_key);
  332. $theme_len = strlen($theme_path);
  333. }
  334. $orphans = variable_get('library_aggregation_orphans', array());
  335. // Callback for defaults.
  336. $function = 'core_library_defaults_' . $type;
  337. // Failsafe.
  338. if (!is_callable($function)) {
  339. return;
  340. }
  341. if (!isset($orphans[$type])) {
  342. // array_diff_key() will require an array here when in learning mode, else
  343. // the function will fail returning an empty result.
  344. $orphans[$type] = array();
  345. }
  346. // In case we are in learning mode, add the new file to manual override
  347. // and save result. The few first hit will cause some SQL overhead and
  348. // some cache rebuild while users browse, but when all files will be
  349. // found, this won't happen anymore.
  350. // Once the maximum files are being found, you should then switch to manual
  351. // mode, it will use the exact same variables we are being populating here
  352. // and will won't attempt to do the array difference here.
  353. // We do it before the real file alteration to ensure that newly found files
  354. // will also be aggregated the first hit.
  355. if ($do_learn) {
  356. $unknown = array_diff_key($array, $orphans[$type]);
  357. // Avoid to store JS settings array, this could cause some unwanted JS
  358. // settings to displayed on wrong pages.
  359. unset($unknown['settings']);
  360. // Proceed only if array is not empty.
  361. if (!empty($unknown)) {
  362. foreach ($unknown as $file => $options) {
  363. $data = array(
  364. 'mode' => LIBRARY_AGGREGATE_ALL,
  365. 'group' => $options['group'],
  366. 'exclude' => _core_library_must_exclude($type, $file, $options),
  367. );
  368. // No exclusion mode forces us to keep the full original options
  369. // instead of allowing us to override it. This also means that the
  370. // file will still always be included whatever happens.
  371. if ($data['exclude']) {
  372. $data += $options;
  373. }
  374. $orphans[$type][$file] = $data;
  375. }
  376. // Save new orphans.
  377. variable_set('library_aggregation_orphans', $orphans);
  378. }
  379. }
  380. // Merge array with our own values.
  381. foreach ($orphans[$type] as $file => $options) {
  382. // Whatever happens, do not include files from another theme. We must
  383. // proceed this check as a side effect of learning all files, including
  384. // those from other themes.
  385. if ('css' === $type && isset($options['group']) && CSS_THEME === $options['group'] && strncmp($theme_path, $file, $theme_len)) {
  386. unset($array[$file]);
  387. }
  388. else {
  389. // If entry does not already exists, add it in order for the core to use
  390. // it for full aggregation.
  391. if (!isset($array[$file])) {
  392. $array[$file] = $options + array('data' => $file);
  393. }
  394. if (!$options['exclude']) {
  395. // Merge already loaded with our own options to force aggregation.
  396. $function($array[$file]);
  397. // File destination may have changed.
  398. if ($array[$file]['data'] != $file) {
  399. $array[$array[$file]['data']] = $array[$file];
  400. unset($array[$file]);
  401. }
  402. }
  403. }
  404. }
  405. }
  406. /**
  407. * Implements hook_js_alter().
  408. */
  409. function core_library_js_alter(&$javascript) {
  410. // We are going to use this hook instead of the hook_init() implementation
  411. // in order to force some settings, because other drupal_add_js() calls will
  412. // override our changes.
  413. if (core_library_bypass_mode()) {
  414. _core_library_merge($javascript, 'js');
  415. }
  416. }
  417. /**
  418. * Implements hook_css_alter().
  419. */
  420. function core_library_css_alter(&$css) {
  421. // We are going to use this hook instead of the hook_init() implementation
  422. // in order to force some settings, because other drupal_add_js() calls will
  423. // override our changes.
  424. if (core_library_bypass_mode()) {
  425. _core_library_merge($css, 'css');
  426. }
  427. }
  428. /**
  429. * Implements hook_help().
  430. */
  431. function core_library_help($path, $arg) {
  432. switch ($path) {
  433. case 'admin/config/development/library':
  434. $messages[] = t("This page allows you to configure advanced CSS and JS files aggregation rules. The Core Library module assumes that aggregating all CSS and JS files together on every page -even if they are not being always used- will provide better CSS and JS files cache hit over a long term usage and save more bandwith than core conditional aggregation algorithm.");
  435. $messages[] = t("Notice that when changing the aggregation mode you need to refresh your aggressive cache page, if enabled. The module won't do it by itself and will let you do so to ensure it won't happen during frequentation pikes.");
  436. if (variable_get('core_library_updated', FALSE)) {
  437. drupal_set_message(t("The Core Library module has been updated and requires you to reset the learnt files so far. If you don't experience PHP notices, CSS malfunctions, or JS errors you can safely ignore this message."), 'error');
  438. }
  439. break;
  440. }
  441. }