PageRenderTime 58ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/css_js.php

http://wowroster.googlecode.com/
PHP | 485 lines | 444 code | 15 blank | 26 comment | 21 complexity | b3375c97b9740047415e75cea1815a74 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * WoWRoster.net WoWRoster
  4. *
  5. * CSS and Javascript Aggregation
  6. *
  7. * @copyright 2002-2011 WoWRoster.net
  8. * @license http://www.gnu.org/licenses/gpl.html Licensed under the GNU General Public License v3.
  9. * @version SVN: $Id: css_js.php 2345 2011-09-10 08:39:19Z c.treyce@gmail.com $
  10. * @link http://www.wowroster.net
  11. * @since File available since Release 2.2.0
  12. * @package WoWRoster
  13. */
  14. if (!defined('IN_ROSTER')) {
  15. exit('Detected invalid access to this file!');
  16. }
  17. function roster_add_js( $data = NULL , $type = 'module' , $scope = 'header' , $defer = FALSE , $cache = TRUE , $preprocess = TRUE ) {
  18. static $javascript = array();
  19. if (isset($data)) {
  20. // Add base javascript files the first time a Javascript file is added.
  21. if (empty($javascript)) {
  22. $javascript['header'] = array(
  23. 'core' => array(
  24. 'js/jquery.js' => array(
  25. 'cache' => TRUE,
  26. 'defer' => FALSE,
  27. 'preprocess' => TRUE,
  28. ),
  29. 'js/jquery-ui.js' => array(
  30. 'cache' => TRUE,
  31. 'defer' => FALSE,
  32. 'preprocess' => TRUE,
  33. ),
  34. 'js/ui.selectmenu.js' => array(
  35. 'cache' => TRUE,
  36. 'defer' => FALSE,
  37. 'preprocess' => TRUE,
  38. ),
  39. 'js/script.js' => array(
  40. 'cache' => TRUE,
  41. 'defer' => FALSE,
  42. 'preprocess' => TRUE,
  43. ),
  44. 'js/tabcontent.js' => array(
  45. 'cache' => TRUE,
  46. 'defer' => FALSE,
  47. 'preprocess' => TRUE,
  48. ),
  49. 'js/mainjs.js' => array(
  50. 'cache' => TRUE,
  51. 'defer' => FALSE,
  52. 'preprocess' => TRUE,
  53. ),
  54. ),
  55. 'module' => array(),
  56. 'theme' => array(
  57. 'js/overlib.js' => array(
  58. 'cache' => TRUE,
  59. 'defer' => FALSE,
  60. 'preprocess' => TRUE,
  61. ),
  62. ),
  63. 'setting' => array(
  64. array('roster_path' => ROSTER_PATH),
  65. ),
  66. 'inline' => array(),
  67. );
  68. }
  69. if (isset($scope) && !isset($javascript[$scope])) {
  70. $javascript[$scope] = array(
  71. 'core' => array(),
  72. 'module' => array(),
  73. 'theme' => array(),
  74. 'setting' => array(),
  75. 'inline' => array(),
  76. );
  77. }
  78. if (isset($type) && isset($scope) && !isset($javascript[$scope][$type])) {
  79. $javascript[$scope][$type] = array();
  80. }
  81. switch ($type) {
  82. case 'setting':
  83. $javascript[$scope][$type][] = $data;
  84. break;
  85. case 'inline':
  86. $javascript[$scope][$type][] = array(
  87. 'code' => $data,
  88. 'defer' => $defer,
  89. );
  90. break;
  91. default:
  92. // If cache is FALSE, don't preprocess the JS file.
  93. $javascript[$scope][$type][$data] = array(
  94. 'cache' => $cache,
  95. 'defer' => $defer,
  96. 'preprocess' => (!$cache ? FALSE : $preprocess),
  97. );
  98. }
  99. }
  100. if (isset($scope)) {
  101. if (isset($javascript[$scope])) {
  102. return $javascript[$scope];
  103. }
  104. else {
  105. return array();
  106. }
  107. }
  108. else {
  109. return $javascript;
  110. }
  111. }
  112. function roster_get_js($scope = 'header', $javascript = NULL) {
  113. global $roster;
  114. if (!isset($javascript)) {
  115. $javascript = roster_add_js(NULL, NULL, $scope);
  116. }
  117. if (empty($javascript)) {
  118. return '';
  119. }
  120. $output = '';
  121. $preprocessed = '';
  122. $no_preprocess = array(
  123. 'core' => '',
  124. 'module' => '',
  125. 'theme' => '',
  126. );
  127. $files = array();
  128. $preprocess_js = isset($roster->config['preprocess_js']) ? $roster->config['preprocess_js'] : 0;
  129. $is_writable = is_dir(ROSTER_CACHEDIR) && is_writable(ROSTER_CACHEDIR);
  130. // A dummy query-string is added to filenames, to gain control over
  131. // browser-caching. The string changes on every update or full cache
  132. // flush, forcing browsers to load a new copy of the files, as the
  133. // URL changed. Files that should not be cached (see roster_add_js())
  134. // get time() as query-string instead, to enforce reload on every
  135. // page request.
  136. $query_string = '?' . (isset($roster->config['css_js_query_string']) ? substr($roster->config['css_js_query_string'], 0, 1) : 0);
  137. //base_convert(REQUEST_TIME, 10, 36);
  138. // For inline Javascript to validate as XHTML, all Javascript containing
  139. // XHTML needs to be wrapped in CDATA. To make that backwards compatible
  140. // with HTML 4, we need to comment out the CDATA-tag.
  141. $embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
  142. $embed_suffix = "\n//--><!]]>\n";
  143. foreach ($javascript as $type => $data) {
  144. if (!$data) {
  145. continue;
  146. }
  147. switch ($type) {
  148. case 'setting':
  149. $output .= '<script type="text/javascript">' . $embed_prefix . 'var roster_js = ' . roster_to_js(call_user_func_array('array_merge_recursive', $data)) . ';' . $embed_suffix . "</script>\n";
  150. break;
  151. case 'inline':
  152. foreach ($data as $info) {
  153. $output .= '<script type="text/javascript"' . ($info['defer'] ? ' defer="defer"' : '') . '>' . $embed_prefix . $info['code'] . $embed_suffix . "</script>\n";
  154. }
  155. break;
  156. default:
  157. // If JS preprocessing is off, we still need to output the scripts.
  158. // Additionally, go through any remaining scripts if JS preprocessing is on and output the non-cached ones.
  159. foreach ($data as $path => $info) {
  160. if (!$info['preprocess'] || !$is_writable || !$preprocess_js) {
  161. $no_preprocess[$type] .= '<script type="text/javascript"' . ($info['defer'] ? ' defer="defer"' : '') . ' src="' . ROSTER_PATH . $path . ($info['cache'] ? $query_string : '?' . time()) . "\"></script>\n";
  162. }
  163. else {
  164. $files[$path] = $info;
  165. }
  166. }
  167. }
  168. }
  169. // Aggregate any remaining JS files that haven't already been output.
  170. if ($is_writable && $preprocess_js && count($files) > 0) {
  171. // Prefix filename to prevent blocking by firewalls which reject files
  172. // starting with "ad*".
  173. $filename = 'js_' . md5(serialize($files) . $query_string) . '.js';
  174. $preprocess_file = roster_build_js_cache($files, $filename);
  175. $preprocessed .= '<script type="text/javascript" src="' . ROSTER_PATH . $preprocess_file . '"></script>' . "\n";
  176. }
  177. // Keep the order of JS files consistent as some are preprocessed and others are not.
  178. // Make sure any inline or JS setting variables appear last after libraries have loaded.
  179. $output = $preprocessed . implode('', $no_preprocess) . $output;
  180. return $output;
  181. }
  182. function roster_to_js($var) {
  183. switch (gettype($var)) {
  184. case 'boolean':
  185. return $var ? 'true' : 'false'; // Lowercase necessary!
  186. case 'integer':
  187. case 'double':
  188. return $var;
  189. case 'resource':
  190. case 'string':
  191. return '"' . str_replace(array("\r", "\n", "<", ">", "&"),
  192. array('\r', '\n', '\x3c', '\x3e', '\x26'),
  193. addslashes($var)) . '"';
  194. case 'array':
  195. // Arrays in JSON can't be associative. If the array is empty or if it
  196. // has sequential whole number keys starting with 0, it's not associative
  197. // so we can go ahead and convert it as an array.
  198. if (empty($var) || array_keys($var) === range(0, sizeof($var) - 1)) {
  199. $output = array();
  200. foreach ($var as $v) {
  201. $output[] = roster_to_js($v);
  202. }
  203. return '[ ' . implode(', ', $output) . ' ]';
  204. }
  205. // Otherwise, fall through to convert the array as an object.
  206. case 'object':
  207. $output = array();
  208. foreach ($var as $k => $v) {
  209. $output[] = roster_to_js(strval($k)) . ': ' . roster_to_js($v);
  210. }
  211. return '{ ' . implode(', ', $output) . ' }';
  212. default:
  213. return 'null';
  214. }
  215. }
  216. function roster_build_js_cache($files, $filename) {
  217. $contents = '';
  218. if (!file_exists(ROSTER_CACHEDIR . $filename)) {
  219. // Build aggregate JS file.
  220. foreach ($files as $path => $info) {
  221. if ($info['preprocess']) {
  222. // Append a ';' and a newline after each JS file to prevent them from running together.
  223. $contents .= file_get_contents($path) . ";\n";
  224. }
  225. }
  226. // Create the JS file.
  227. file_writer(ROSTER_CACHEDIR . $filename, $contents);
  228. }
  229. return 'cache/' . $filename;
  230. }
  231. function roster_add_css($path = NULL, $type = 'module', $media = 'all', $preprocess = TRUE) {
  232. static $css = array();
  233. // Create an array of CSS files for each media type first, since each type needs to be served
  234. // to the browser differently.
  235. if (isset($path)) {
  236. // This check is necessary to ensure proper cascading of styles and is faster than an asort().
  237. if (!isset($css[$media])) {
  238. $css[$media] = array(
  239. 'module' => array(),
  240. 'theme' => array(),
  241. );
  242. }
  243. $css[$media][$type][$path] = $preprocess;
  244. }
  245. return $css;
  246. }
  247. function roster_get_css($css = NULL) {
  248. global $roster;
  249. $output = '';
  250. if (!isset($css)) {
  251. $css = roster_add_css();
  252. }
  253. $no_module_preprocess = '';
  254. $no_theme_preprocess = '';
  255. $preprocess_css = isset($roster->config['preprocess_css']) ? $roster->config['preprocess_css'] : 0;
  256. $is_writable = is_dir(ROSTER_CACHEDIR) && is_writable(ROSTER_CACHEDIR);
  257. // A dummy query-string is added to filenames, to gain control over
  258. // browser-caching. The string changes on every update or full cache
  259. // flush, forcing browsers to load a new copy of the files, as the
  260. // URL changed.
  261. $query_string = '?' . (isset($roster->config['css_js_query_string']) ? substr($roster->config['css_js_query_string'], 0, 1) : 0);
  262. foreach ($css as $media => $types) {
  263. // If CSS preprocessing is off, we still need to output the styles.
  264. // Additionally, go through any remaining styles if CSS preprocessing is on and output the non-cached ones.
  265. foreach ($types as $type => $files) {
  266. if ($type == 'module') {
  267. // Setup theme overrides for module styles.
  268. $theme_styles = array();
  269. foreach (array_keys($css[$media]['theme']) as $theme_style) {
  270. $theme_styles[] = basename($theme_style);
  271. }
  272. }
  273. foreach ($types[$type] as $file => $preprocess) {
  274. // If the theme supplies its own style using the name of the module style, skip its inclusion.
  275. // This includes any RTL styles associated with its main LTR counterpart.
  276. if ($type == 'module') {
  277. // Unset the file to prevent its inclusion when CSS aggregation is enabled.
  278. unset($types[$type][$file]);
  279. continue;
  280. }
  281. // Only include the stylesheet if it exists.
  282. if (file_exists(ROSTER_BASE . $file)) {
  283. if (!$preprocess || !($is_writable && $preprocess_css)) {
  284. // If a CSS file is not to be preprocessed and it's a module CSS file, it needs to *always* appear at the *top*,
  285. // regardless of whether preprocessing is on or off.
  286. if (!$preprocess && $type == 'module') {
  287. $no_module_preprocess .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . ROSTER_PATH . $file . $query_string . '" />' . "\n";
  288. }
  289. // If a CSS file is not to be preprocessed and it's a theme CSS file, it needs to *always* appear at the *bottom*,
  290. // regardless of whether preprocessing is on or off.
  291. else if (!$preprocess && $type == 'theme') {
  292. $no_theme_preprocess .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . ROSTER_PATH . $file . $query_string . '" />' . "\n";
  293. }
  294. else {
  295. $output .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . ROSTER_PATH . $file . $query_string . '" />' . "\n";
  296. }
  297. }
  298. }
  299. }
  300. }
  301. if ($is_writable && $preprocess_css) {
  302. // Prefix filename to prevent blocking by firewalls which reject files
  303. // starting with "ad*".
  304. $filename = 'css_' . md5(serialize($types) . $query_string) . '.css';
  305. $preprocess_file = roster_build_css_cache($types, $filename);
  306. $output .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . ROSTER_PATH . $preprocess_file . '" />' . "\n";
  307. }
  308. }
  309. return $no_module_preprocess . $output . $no_theme_preprocess;
  310. }
  311. function roster_build_css_cache($types, $filename) {
  312. $data = '';
  313. if (!file_exists(ROSTER_CACHEDIR . $filename)) {
  314. // Build aggregate CSS file.
  315. foreach ($types as $type) {
  316. foreach ($type as $file => $cache) {
  317. if ($cache) {
  318. $contents = roster_load_stylesheet($file, TRUE);
  319. // Return the path to where this CSS file originated from.
  320. $base = ROSTER_PATH . dirname($file) . '/';
  321. _roster_build_css_path(NULL, $base);
  322. // Prefix all paths within this CSS file, ignoring external and absolute paths.
  323. $data .= preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', '_roster_build_css_path', $contents);
  324. }
  325. }
  326. }
  327. // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import,
  328. // @import rules must proceed any other style, so we move those to the top.
  329. $regexp = '/@import[^;]+;/i';
  330. preg_match_all($regexp, $data, $matches);
  331. $data = preg_replace($regexp, '', $data);
  332. $data = implode('', $matches[0]) . $data;
  333. // Create the CSS file.
  334. file_writer(ROSTER_CACHEDIR . $filename, $data);
  335. }
  336. return 'cache/' . $filename;
  337. }
  338. function roster_load_stylesheet($file, $optimize = NULL) {
  339. static $_optimize;
  340. // Store optimization parameter for preg_replace_callback with nested @import loops.
  341. if (isset($optimize)) {
  342. $_optimize = $optimize;
  343. }
  344. $contents = '';
  345. if (file_exists($file)) {
  346. // Load the local CSS stylesheet.
  347. $contents = file_get_contents($file);
  348. // Change to the current stylesheet's directory.
  349. $cwd = getcwd();
  350. chdir(dirname($file));
  351. // Replaces @import commands with the actual stylesheet content.
  352. // This happens recursively but omits external files.
  353. $contents = preg_replace_callback('/@import\s*(?:url\()?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\)?;/', '_roster_load_stylesheet', $contents);
  354. // Remove multiple charset declarations for standards compliance (and fixing Safari problems).
  355. $contents = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $contents);
  356. if ($_optimize) {
  357. // Perform some safe CSS optimizations.
  358. // Regexp to match comment blocks.
  359. $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
  360. // Regexp to match double quoted strings.
  361. $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
  362. // Regexp to match single quoted strings.
  363. $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
  364. $contents = preg_replace_callback(
  365. "<$double_quot|$single_quot|$comment>Ss", // Match all comment blocks along
  366. "_roster_process_comment", // with double/single quoted strings
  367. $contents); // and feed them to _process_comment().
  368. $contents = preg_replace(
  369. '<\s*([@{}:;,]|\)\s|\s\()\s*>S', // Remove whitespace around separators,
  370. '\1', $contents); // but keep space around parentheses.
  371. // End the file with a new line.
  372. $contents .= "\n";
  373. }
  374. // Change back directory.
  375. chdir($cwd);
  376. }
  377. return $contents;
  378. }
  379. function _roster_load_stylesheet($matches) {
  380. $filename = $matches[1];
  381. // Load the imported stylesheet and replace @import commands in there as well.
  382. $file = roster_load_stylesheet($filename);
  383. // Determine the file's directory.
  384. $directory = dirname($filename);
  385. // If the file is in the current directory, make sure '.' doesn't appear in
  386. // the url() path.
  387. $directory = $directory == '.' ? '' : $directory . '/';
  388. // Alter all internal url() paths. Leave external paths alone. We don't need
  389. // to normalize absolute paths here (i.e. remove folder/... segments) because
  390. // that will be done later.
  391. return preg_replace('/url\s*\(([\'"]?)(?![a-z]+:|\/+)/i', 'url(\1' . $directory, $file);
  392. }
  393. function _roster_build_css_path($matches, $base = NULL) {
  394. static $_base;
  395. // Store base path for preg_replace_callback.
  396. if (isset($base)) {
  397. $_base = $base;
  398. }
  399. // Prefix with base and remove '../' segments where possible.
  400. $path = $_base . $matches[1];
  401. $last = '';
  402. while ($path != $last) {
  403. $last = $path;
  404. $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
  405. }
  406. return 'url(' . $path . ')';
  407. }
  408. function _roster_process_comment($matches) {
  409. static $keep_nextone = FALSE;
  410. // Quoted string, keep it.
  411. if ($matches[0][0] == "'" || $matches[0][0] == '"') {
  412. return $matches[0];
  413. }
  414. // End of IE-Mac hack, keep it.
  415. if ($keep_nextone) {
  416. $keep_nextone = FALSE;
  417. return $matches[0];
  418. }
  419. switch (strrpos($matches[0], '\\')) {
  420. case FALSE:
  421. // No backslash, strip it.
  422. return '';
  423. case strlen(preg_replace("/[\x80-\xBF]/", '', $matches[0])) -3:
  424. // Ends with \*/ so is a multi line IE-Mac hack, keep the next one also.
  425. $keep_nextone = TRUE;
  426. return '/*_\*/';
  427. default:
  428. // Single line IE-Mac hack.
  429. return '/*\_*/';
  430. }
  431. }
  432. ?>