1 <?php
2
3 /**
4 * Requirements tracker, for javascript and css.
5 * @todo Document the requirements tracker, and discuss it with the others.
6 *
7 * @package sapphire
8 * @subpackage view
9 */
10 class Requirements {
11
12 /**
13 * Enable combining of css/javascript files.
14 * @param boolean $enable
15 */
16 public static function set_combined_files_enabled($enable) {
17 self::backend()->set_combined_files_enabled($enable);
18 }
19
20 /**
21 * Checks whether combining of css/javascript files is enabled.
22 * @return boolean
23 */
24 public static function get_combined_files_enabled() {
25 return self::backend()->get_combined_files_enabled();
26 }
27
28 /**
29 * Set the relative folder e.g. "assets" for where to store combined files
30 * @param string $folder Path to folder
31 */
32 public static function set_combined_files_folder($folder) {
33 self::backend()->setCombinedFilesFolder($folder);
34 }
35
36 /**
37 * Set whether we want to suffix requirements with the time /
38 * location on to the requirements
39 *
40 * @param bool
41 */
42 public static function set_suffix_requirements($var) {
43 self::backend()->set_suffix_requirements($var);
44 }
45
46 /**
47 * Return whether we want to suffix requirements
48 *
49 * @return bool
50 */
51 public static function get_suffix_requirements() {
52 return self::backend()->get_suffix_requirements();
53 }
54
55 /**
56 * Instance of requirements for storage
57 *
58 * @var Requirements
59 */
60 private static $backend = null;
61
62 public static function backend() {
63 if(!self::$backend) {
64 self::$backend = new Requirements_Backend();
65 }
66 return self::$backend;
67 }
68
69 /**
70 * Setter method for changing the Requirements backend
71 *
72 * @param Requirements $backend
73 */
74 public static function set_backend(Requirements_Backend $backend) {
75 self::$backend = $backend;
76 }
77
78 /**
79 * Register the given javascript file as required.
80 *
81 * See {@link Requirements_Backend::javascript()} for more info
82 *
83 */
84 static function javascript($file) {
85 self::backend()->javascript($file);
86 }
87
88 /**
89 * Add the javascript code to the header of the page
90 *
91 * See {@link Requirements_Backend::customScript()} for more info
92 * @param script The script content
93 * @param uniquenessID Use this to ensure that pieces of code only get added once.
94 */
95 static function customScript($script, $uniquenessID = null) {
96 self::backend()->customScript($script, $uniquenessID);
97 }
98
99 /**
100 * Include custom CSS styling to the header of the page.
101 *
102 * See {@link Requirements_Backend::customCSS()}
103 *
104 * @param string $script CSS selectors as a string (without <style> tag enclosing selectors).
105 * @param int $uniquenessID Group CSS by a unique ID as to avoid duplicate custom CSS in header
106 */
107 static function customCSS($script, $uniquenessID = null) {
108 self::backend()->customCSS($script, $uniquenessID);
109 }
110
111 /**
112 * Add the following custom code to the <head> section of the page.
113 * See {@link Requirements_Backend::insertHeadTags()}
114 *
115 * @param string $html
116 * @param string $uniquenessID
117 */
118 static function insertHeadTags($html, $uniquenessID = null) {
119 self::backend()->insertHeadTags($html, $uniquenessID);
120 }
121
122 /**
123 * Load the given javascript template with the page.
124 * See {@link Requirements_Backend::javascriptTemplate()}
125 *
126 * @param file The template file to load.
127 * @param vars The array of variables to load. These variables are loaded via string search & replace.
128 */
129 static function javascriptTemplate($file, $vars, $uniquenessID = null) {
130 self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
131 }
132
133 /**
134 * Register the given stylesheet file as required.
135 * See {@link Requirements_Backend::css()}
136 *
137 * @param $file String Filenames should be relative to the base, eg, 'sapphire/javascript/tree/tree.css'
138 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
139 * @see http://www.w3.org/TR/REC-CSS2/media.html
140 */
141 static function css($file, $media = null) {
142 self::backend()->css($file, $media);
143 }
144
145 /**
146 * Register the given "themeable stylesheet" as required. See {@link Requirements_Backend::themedCSS()}
147 *
148 * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile"
149 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
150 */
151 static function themedCSS($name, $media = null) {
152 return self::backend()->themedCSS($name, $media);
153 }
154
155 /**
156 * Clear either a single or all requirements.
157 * Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}.
158 *
159 * See {@link Requirements_Backend::clear()}
160 *
161 * @param $file String
162 */
163 static function clear($fileOrID = null) {
164 self::backend()->clear($fileOrID);
165 }
166
167 /**
168 * Blocks inclusion of a specific file
169 * See {@link Requirements_Backend::block()}
170 *
171 * @param unknown_type $fileOrID
172 */
173 static function block($fileOrID) {
174 self::backend()->block($fileOrID);
175 }
176
177 /**
178 * Removes an item from the blocking-list.
179 * See {@link Requirements_Backend::unblock()}
180 *
181 * @param string $fileOrID
182 */
183 static function unblock($fileOrID) {
184 self::backend()->unblock($fileOrID);
185 }
186
187 /**
188 * Removes all items from the blocking-list.
189 * See {@link Requirements_Backend::unblock_all()}
190 */
191 static function unblock_all() {
192 self::backend()->unblock_all();
193 }
194
195 /**
196 * Restore requirements cleared by call to Requirements::clear
197 * See {@link Requirements_Backend::restore()}
198 */
199 static function restore() {
200 self::backend()->restore();
201 }
202
203 /**
204 * Update the given HTML content with the appropriate include tags for the registered
205 * requirements.
206 * See {@link Requirements_Backend::includeInHTML()} for more information.
207 *
208 * @param string $templateFilePath Absolute path for the *.ss template file
209 * @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}.
210 * @return string HTML content thats augumented with the requirements before the closing <head> tag.
211 */
212 static function includeInHTML($templateFile, $content) {
213 return self::backend()->includeInHTML($templateFile, $content);
214 }
215
216 static function include_in_response(SS_HTTPResponse $response) {
217 return self::backend()->include_in_response($response);
218 }
219
220 /**
221 * Add i18n files from the given javascript directory.
222 * @param $langDir The javascript lang directory, relative to the site root, e.g., 'sapphire/javascript/lang'
223 *
224 * See {@link Requirements_Backend::add_i18n_javascript()} for more information.
225 */
226 public static function add_i18n_javascript($langDir) {
227 return self::backend()->add_i18n_javascript($langDir);
228 }
229
230 /**
231 * Concatenate several css or javascript files into a single dynamically generated file.
232 * See {@link Requirements_Backend::combine_files()} for more info.
233 *
234 * @param string $combinedFileName
235 * @param array $files
236 */
237 static function combine_files($combinedFileName, $files) {
238 self::backend()->combine_files($combinedFileName, $files);
239 }
240
241 /**
242 * Returns all combined files.
243 * See {@link Requirements_Backend::get_combine_files()}
244 *
245 * @return array
246 */
247 static function get_combine_files() {
248 return self::backend()->get_combine_files();
249 }
250
251 /**
252 * Deletes all dynamically generated combined files from the filesystem.
253 * See {@link Requirements_Backend::delete_combine_files()}
254 *
255 * @param string $combinedFileName If left blank, all combined files are deleted.
256 */
257 static function delete_combined_files($combinedFileName = null) {
258 return self::backend()->delete_combined_files($combinedFileName);
259 }
260
261
262 /**
263 * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
264 */
265 static function clear_combined_files() {
266 self::backend()->clear_combined_files();
267 }
268
269 /**
270 * See {@link combine_files()}.
271 */
272 static function process_combined_files() {
273 return self::backend()->process_combined_files();
274 }
275
276 /**
277 * Returns all custom scripts
278 * See {@link Requirements_Backend::get_custom_scripts()}
279 *
280 * @return array
281 */
282 static function get_custom_scripts() {
283 return self::backend()->get_custom_scripts();
284 }
285
286 /**
287 * Set whether you want to write the JS to the body of the page or
288 * in the head section
289 *
290 * @see Requirements_Backend::set_write_js_to_body()
291 * @param boolean
292 */
293 static function set_write_js_to_body($var) {
294 self::backend()->set_write_js_to_body($var);
295 }
296
297 static function debug() {
298 return self::backend()->debug();
299 }
300
301 }
302
303 /**
304 * @package sapphire
305 * @subpackage view
306 */
307 class Requirements_Backend {
308
309 /**
310 * Do we want requirements to suffix onto the requirement link
311 * tags for caching or is it disabled. Getter / Setter available
312 * through {@link Requirements::set_suffix_requirements()}
313 *
314 * @var bool
315 */
316 protected $suffix_requirements = true;
317
318 /**
319 * Enable combining of css/javascript files.
320 *
321 * @var boolean
322 */
323 protected $combined_files_enabled = true;
324
325 /**
326 * Paths to all required .js files relative to the webroot.
327 *
328 * @var array $javascript
329 */
330 protected $javascript = array();
331
332 /**
333 * Paths to all required .css files relative to the webroot.
334 *
335 * @var array $css
336 */
337 protected $css = array();
338
339 /**
340 * All custom javascript code that is inserted
341 * directly at the bottom of the HTML <head> tag.
342 *
343 * @var array $customScript
344 */
345 protected $customScript = array();
346
347 /**
348 * All custom CSS rules which are inserted
349 * directly at the bottom of the HTML <head> tag.
350 *
351 * @var array $customCSS
352 */
353 protected $customCSS = array();
354
355 /**
356 * All custom HTML markup which is added before
357 * the closing <head> tag, e.g. additional metatags.
358 * This is preferred to entering tags directly into
359 */
360 protected $customHeadTags = array();
361
362 /**
363 * Remembers the filepaths of all cleared Requirements
364 * through {@link clear()}.
365 *
366 * @var array $disabled
367 */
368 protected $disabled = array();
369
370 /**
371 * The filepaths (relative to webroot) or
372 * uniquenessIDs of any included requirements
373 * which should be blocked when executing {@link inlcudeInHTML()}.
374 * This is useful to e.g. prevent core classes to modifying
375 * Requirements without subclassing the entire functionality.
376 * Use {@link unblock()} or {@link unblock_all()} to revert changes.
377 *
378 * @var array $blocked
379 */
380 protected $blocked = array();
381
382 /**
383 * See {@link combine_files()}.
384 *
385 * @var array $combine_files
386 */
387 public $combine_files = array();
388
389 /**
390 * Using the JSMin library to minify any
391 * javascript file passed to {@link combine_files()}.
392 *
393 * @var boolean
394 */
395 public $combine_js_with_jsmin = true;
396
397 /**
398 * By default, combined files are stored in assets/_combinedfiles.
399 * Set this by calling Requirements::set_combined_files_folder()
400 * @var string
401 */
402 protected $combinedFilesFolder = 'assets/_combinedfiles';
403
404 /**
405 * Put all javascript includes at the bottom of the template
406 * before the closing <body> tag instead of the <head> tag.
407 * This means script downloads won't block other HTTP-requests,
408 * which can be a performance improvement.
409 * Caution: Doesn't work when modifying the DOM from those external
410 * scripts without listening to window.onload/document.ready
411 * (e.g. toplevel document.write() calls).
412 *
413 * @see http://developer.yahoo.com/performance/rules.html#js_bottom
414 *
415 * @var boolean
416 */
417 public $write_js_to_body = true;
418
419 function set_combined_files_enabled($enable) {
420 $this->combined_files_enabled = (bool) $enable;
421 }
422
423 function get_combined_files_enabled() {
424 return $this->combined_files_enabled;
425 }
426
427 function setCombinedFilesFolder($folder) {
428 $this->combinedFilesFolder = $folder;
429 }
430
431 /**
432 * Set whether we want to suffix requirements with the time /
433 * location on to the requirements
434 *
435 * @param bool
436 */
437 function set_suffix_requirements($var) {
438 $this->suffix_requirements = $var;
439 }
440
441 /**
442 * Return whether we want to suffix requirements
443 *
444 * @return bool
445 */
446 function get_suffix_requirements() {
447 return $this->suffix_requirements;
448 }
449
450 /**
451 * Set whether you want the files written to the head or the body. It
452 * writes to the body by default which can break some scripts
453 *
454 * @param boolean
455 */
456 public function set_write_js_to_body($var) {
457 $this->write_js_to_body = $var;
458 }
459 /**
460 * Register the given javascript file as required.
461 * Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js'
462 */
463
464 public function javascript($file) {
465 $this->javascript[$file] = true;
466 }
467
468 /**
469 * Returns an array of all included javascript
470 *
471 * @return array
472 */
473 public function get_javascript() {
474 return array_keys(array_diff_key($this->javascript,$this->blocked));
475 }
476
477 /**
478 * Add the javascript code to the header of the page
479 * @todo Make Requirements automatically put this into a separate file :-)
480 * @param script The script content
481 * @param uniquenessID Use this to ensure that pieces of code only get added once.
482 */
483 public function customScript($script, $uniquenessID = null) {
484 if($uniquenessID) $this->customScript[$uniquenessID] = $script;
485 else $this->customScript[] = $script;
486
487 $script .= "\n";
488 }
489
490 /**
491 * Include custom CSS styling to the header of the page.
492 *
493 * @param string $script CSS selectors as a string (without <style> tag enclosing selectors).
494 * @param int $uniquenessID Group CSS by a unique ID as to avoid duplicate custom CSS in header
495 */
496 function customCSS($script, $uniquenessID = null) {
497 if($uniquenessID) $this->customCSS[$uniquenessID] = $script;
498 else $this->customCSS[] = $script;
499 }
500
501 /**
502 * Add the following custom code to the <head> section of the page.
503 *
504 * @param string $html
505 * @param string $uniquenessID
506 */
507 function insertHeadTags($html, $uniquenessID = null) {
508 if($uniquenessID) $this->customHeadTags[$uniquenessID] = $html;
509 else $this->customHeadTags[] = $html;
510 }
511
512 /**
513 * Load the given javascript template with the page.
514 * @param file The template file to load.
515 * @param vars The array of variables to load. These variables are loaded via string search & replace.
516 */
517 function javascriptTemplate($file, $vars, $uniquenessID = null) {
518 $script = file_get_contents(Director::getAbsFile($file));
519 $search = array();
520 $replace = array();
521
522 if($vars) foreach($vars as $k => $v) {
523 $search[] = '$' . $k;
524 $replace[] = str_replace("\\'","'", Convert::raw2js($v));
525 }
526
527 $script = str_replace($search, $replace, $script);
528 $this->customScript($script, $uniquenessID);
529 }
530
531 /**
532 * Register the given stylesheet file as required.
533 *
534 * @param $file String Filenames should be relative to the base, eg, 'sapphire/javascript/tree/tree.css'
535 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
536 * @see http://www.w3.org/TR/REC-CSS2/media.html
537 */
538 function css($file, $media = null) {
539 $this->css[$file] = array(
540 "media" => $media
541 );
542 }
543
544 function get_css() {
545 return array_diff_key($this->css, $this->blocked);
546 }
547
548 /**
549 * Needed to actively prevent the inclusion of a file,
550 * e.g. when using your own prototype.js.
551 * Blocking should only be used as an exception, because
552 * it is hard to trace back. You can just block items with an
553 * ID, so make sure you add an unique identifier to customCSS() and customScript().
554 *
555 * @param string $fileOrID
556 */
557 function block($fileOrID) {
558 $this->blocked[$fileOrID] = $fileOrID;
559 }
560
561 /**
562 * Clear either a single or all requirements.
563 * Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}.
564 *
565 * @param $file String
566 */
567 function clear($fileOrID = null) {
568 if($fileOrID) {
569 foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
570 if(isset($this->{$type}[$fileOrID])) {
571 $this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
572 unset($this->{$type}[$fileOrID]);
573 }
574 }
575 } else {
576 $this->disabled['javascript'] = $this->javascript;
577 $this->disabled['css'] = $this->css;
578 $this->disabled['customScript'] = $this->customScript;
579 $this->disabled['customCSS'] = $this->customCSS;
580 $this->disabled['customHeadTags'] = $this->customHeadTags;
581
582 $this->javascript = array();
583 $this->css = array();
584 $this->customScript = array();
585 $this->customCSS = array();
586 $this->customHeadTags = array();
587 }
588 }
589
590 /**
591 * Removes an item from the blocking-list.
592 * CAUTION: Does not "re-add" any previously blocked elements.
593 * @param string $fileOrID
594 */
595 function unblock($fileOrID) {
596 if(isset($this->blocked[$fileOrID])) unset($this->blocked[$fileOrID]);
597 }
598 /**
599 * Removes all items from the blocking-list.
600 */
601 function unblock_all() {
602 $this->blocked = array();
603 }
604
605 /**
606 * Restore requirements cleared by call to Requirements::clear
607 */
608 function restore() {
609 $this->javascript = $this->disabled['javascript'];
610 $this->css = $this->disabled['css'];
611 $this->customScript = $this->disabled['customScript'];
612 $this->customCSS = $this->disabled['customCSS'];
613 $this->customHeadTags = $this->disabled['customHeadTags'];
614 }
615
616 /**
617 * Update the given HTML content with the appropriate include tags for the registered
618 * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
619 * including a <head> tag. The requirements will insert before the closing <head> tag automatically.
620 *
621 * @todo Calculate $prefix properly
622 *
623 * @param string $templateFilePath Absolute path for the *.ss template file
624 * @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}.
625 * @return string HTML content thats augumented with the requirements before the closing <head> tag.
626 */
627 function includeInHTML($templateFile, $content) {
628 if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML");
629
630 if(strpos($content, '</head') !== false && ($this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags)) {
631 $requirements = '';
632 $jsRequirements = '';
633
634 // Combine files - updates $this->javascript and $this->css
635 $this->process_combined_files();
636
637 foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) {
638 $path = $this->path_for_file($file);
639 if($path) {
640 $jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n";
641 }
642 }
643
644 // add all inline javascript *after* including external files which
645 // they might rely on
646 if($this->customScript) {
647 foreach(array_diff_key($this->customScript,$this->blocked) as $script) {
648 $jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
649 $jsRequirements .= "$script\n";
650 $jsRequirements .= "\n//]]>\n</script>\n";
651 }
652 }
653
654 foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
655 $path = $this->path_for_file($file);
656 if($path) {
657 $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : "";
658 $requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n";
659 }
660 }
661
662 foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
663 $requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
664 }
665
666 foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
667 $requirements .= "$customHeadTag\n";
668 }
669
670 if($this->write_js_to_body) {
671 // Remove all newlines from code to preserve layout
672 $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
673
674 // We put script tags into the body, for performance.
675 // If your template already has script tags in the body, then we put our script
676 // tags just before those. Otherwise, we put it at the bottom.
677 $p2 = stripos($content, '<body');
678 $p1 = stripos($content, '<script', (int) $p2);
679 if($p1 !== false && $p1 > $p2) {
680 $content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1);
681 } else {
682 $content = preg_replace("!(</body>)!i", $jsRequirements . "\\1", $content);
683 }
684
685 // Put CSS at the bottom of the head
686 $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content, 1);
687 } else {
688 $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content, 1);
689 $content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content, 1);
690 }
691 }
692
693 if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML");
694
695 return $content;
696 }
697
698 /**
699 * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response
700 */
701 function include_in_response(SS_HTTPResponse $response) {
702 $this->process_combined_files();
703 $jsRequirements = array();
704 $cssRequirements = array();
705
706 foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) {
707 $path = $this->path_for_file($file);
708 if($path) $jsRequirements[] = $path;
709 }
710
711 $response->addHeader('X-Include-JS', implode(',', $jsRequirements));
712
713 foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
714 $path = $this->path_for_file($file);
715 if($path) $cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
716 }
717
718 $response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
719 }
720
721 /**
722 * Add i18n files from the given javascript directory. Sapphire expects that the given directory
723 * will contain a number of java script files named by language: en_US.js, de_DE.js, etc.
724 * @param $langDir The javascript lang directory, relative to the site root, e.g., 'sapphire/javascript/lang'
725 */
726 public function add_i18n_javascript($langDir) {
727 if(i18n::get_js_i18n()) {
728 // Include i18n.js even if no languages are found. The fact that
729 // add_i18n_javascript() was called indicates that the methods in
730 // here are needed.
731 $this->javascript(SAPPHIRE_DIR . '/javascript/i18n.js');
732
733 if(substr($langDir,-1) != '/') $langDir .= '/';
734
735 $this->javascript($langDir . i18n::default_locale() . '.js');
736 $this->javascript($langDir . i18n::get_locale() . '.js');
737
738 // Stub i18n implementation for when i18n is disabled.
739 } else {
740 $this->javascript[SAPPHIRE_DIR . '/javascript/i18nx.js'] = true;
741 }
742 }
743
744 /**
745 * Finds the path for specified file.
746 *
747 * @param string $fileOrUrl
748 * @return string|boolean
749 */
750 protected function path_for_file($fileOrUrl) {
751 if(preg_match('!^(https?|//)!', $fileOrUrl)) {
752 return $fileOrUrl;
753 } elseif(Director::fileExists($fileOrUrl)) {
754 $prefix = Director::absoluteBaseURL();
755 $mtimesuffix = "";
756 $suffix = '';
757 if(strpos($fileOrUrl, '?') !== false) {
758 $suffix = '&' . substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
759 $fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
760 }
761 if($this->suffix_requirements) {
762 $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' . $fileOrUrl);
763 }
764 return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
765 } else {
766 return false;
767 }
768 }
769
770 /**
771 * Concatenate several css or javascript files into a single dynamically generated
772 * file (stored in {@link Director::baseFolder()}). This increases performance
773 * by fewer HTTP requests.
774 *
775 * The combined file is regenerated
776 * based on every file modification time. Optionally a rebuild can be triggered
777 * by appending ?flush=1 to the URL.
778 * If all files to be combined are javascript, we use the external JSMin library
779 * to minify the javascript. This can be controlled by {@link $combine_js_with_jsmin}.
780 *
781 * All combined files will have a comment on the start of each concatenated file
782 * denoting their original position. For easier debugging, we recommend to only
783 * minify javascript if not in development mode ({@link Director::isDev()}).
784 *
785 * CAUTION: You're responsible for ensuring that the load order for combined files
786 * is retained - otherwise combining javascript files can lead to functional errors
787 * in the javascript logic, and combining css can lead to wrong styling inheritance.
788 * Depending on the javascript logic, you also have to ensure that files are not included
789 * in more than one combine_files() call.
790 * Best practice is to include every javascript file in exactly *one* combine_files()
791 * directive to avoid the issues mentioned above - this is enforced by this function.
792 *
793 * CAUTION: Combining CSS Files discards any "media" information.
794 *
795 * Example for combined JavaScript:
796 * <code>
797 * Requirements::combine_files(
798 * 'foobar.js',
799 * array(
800 * 'mysite/javascript/foo.js',
801 * 'mysite/javascript/bar.js',
802 * )
803 * );
804 * </code>
805 *
806 * Example for combined CSS:
807 * <code>
808 * Requirements::combine_files(
809 * 'foobar.css',
810 * array(
811 * 'mysite/javascript/foo.css',
812 * 'mysite/javascript/bar.css',
813 * )
814 * );
815 * </code>
816 *
817 * @see http://code.google.com/p/jsmin-php/
818 *
819 * @todo Should we enforce unique inclusion of files, or leave it to the developer? Can auto-detection cause breaks?
820 *
821 * @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()} by default)
822 * @param array $files Array of filenames relative to the webroot
823 */
824 function combine_files($combinedFileName, $files) {
825 // duplicate check
826 foreach($this->combine_files as $_combinedFileName => $_files) {
827 $duplicates = array_intersect($_files, $files);
828 if($duplicates) {
829 user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates) . " in combined file '{$_combinedFileName}'", E_USER_NOTICE);
830 return false;
831 }
832 }
833
834 $this->combine_files[$combinedFileName] = $files;
835 }
836
837 /**
838 * Returns all combined files.
839 * @return array
840 */
841 function get_combine_files() {
842 return $this->combine_files;
843 }
844
845 /**
846 * Deletes all dynamically generated combined files from the filesystem.
847 *
848 * @param string $combinedFileName If left blank, all combined files are deleted.
849 */
850 function delete_combined_files($combinedFileName = null) {
851 $combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
852 $combinedFolder = ($this->combinedFilesFolder) ? (Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
853 foreach($combinedFiles as $combinedFile => $sourceItems) {
854 $filePath = $combinedFolder . '/' . $combinedFile;
855 if(file_exists($filePath)) {
856 unlink($filePath);
857 }
858 }
859 }
860
861 function clear_combined_files() {
862 $this->combine_files = array();
863 }
864
865 /**
866 * See {@link combine_files()}
867 *
868 */
869 function process_combined_files() {
870 // The class_exists call prevents us from loading SapphireTest.php (slow) just to know that
871 // SapphireTest isn't running :-)
872 if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
873 else $runningTest = false;
874
875 if((Director::isDev() && !$runningTest) || !$this->combined_files_enabled) {
876 return;
877 }
878
879 // FIX by Inxo. JSMin crash javascript for IE6
880 $useragent = $_SERVER['HTTP_USER_AGENT'];
881 if(preg_match('|MSIE ([6].[0-9]{1,2})|',$useragent,$matched)){
882 return;
883 }
884
885 // Make a map of files that could be potentially combined
886 $combinerCheck = array();
887 foreach($this->combine_files as $combinedFile => $sourceItems) {
888 foreach($sourceItems as $sourceItem) {
889 if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
890 user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
891 }
892 $combinerCheck[$sourceItem] = $combinedFile;
893
894 }
895 }
896
897 // Work out the relative URL for the combined files from the base folder
898 $combinedFilesFolder = ($this->combinedFilesFolder) ? ($this->combinedFilesFolder . '/') : '';
899
900 // Figure out which ones apply to this pageview
901 $combinedFiles = array();
902 $newJSRequirements = array();
903 $newCSSRequirements = array();
904 foreach($this->javascript as $file => $dummy) {
905 if(isset($combinerCheck[$file])) {
906 $newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
907 $combinedFiles[$combinerCheck[$file]] = true;
908 } else {
909 $newJSRequirements[$file] = true;
910 }
911 }
912
913 foreach($this->css as $file => $params) {
914 if(isset($combinerCheck[$file])) {
915 $newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
916 $combinedFiles[$combinerCheck[$file]] = true;
917 } else {
918 $newCSSRequirements[$file] = $params;
919 }
920 }
921
922 // Process the combined files
923 $base = Director::baseFolder() . '/';
924 foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) {
925 $fileList = $this->combine_files[$combinedFile];
926 $combinedFilePath = $base . $this->combinedFilesFolder . '/' . $combinedFile;
927
928
929 // Make the folder if necessary
930 if(!file_exists(dirname($combinedFilePath))) {
931 Filesystem::makeFolder(dirname($combinedFilePath));
932 }
933
934 // If the file isn't writebale, don't even bother trying to make the combined file
935 // Complex test because is_writable fails if the file doesn't exist yet.
936 if((file_exists($combinedFilePath) && !is_writable($combinedFilePath)) ||
937 (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath)))) {
938 user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'", E_USER_WARNING);
939 continue;
940 }
941
942 // Determine if we need to build the combined include
943 if(file_exists($combinedFilePath) && !isset($_GET['flush'])) {
944 // file exists, check modification date of every contained file
945 $srcLastMod = 0;
946 foreach($fileList as $file) {
947 $srcLastMod = max(filemtime($base . $file), $srcLastMod);
948 }
949 $refresh = $srcLastMod > filemtime($combinedFilePath);
950 } else {
951 // file doesn't exist, or refresh was explicitly required
952 $refresh = true;
953 }
954
955 if(!$refresh) continue;
956
957 $combinedData = "";
958 foreach(array_diff($fileList, $this->blocked) as $file) {
959 $fileContent = file_get_contents($base . $file);
960 // if we have a javascript file and jsmin is enabled, minify the content
961 $isJS = stripos($file, '.js');
962 if($isJS && $this->combine_js_with_jsmin) {
963 require_once('thirdparty/jsmin/jsmin.php');
964
965 increase_time_limit_to();
966 $fileContent = JSMin::minify($fileContent);
967 }
968 // write a header comment for each file for easier identification and debugging
969 // also the semicolon between each file is required for jQuery to be combinable properly
970 $combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n".($isJS ? ';' : '')."\n";
971 }
972
973 $successfulWrite = false;
974 $fh = fopen($combinedFilePath, 'wb');
975 if($fh) {
976 if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true;
977 fclose($fh);
978 unset($fh);
979 }
980
981 // Unsuccessful write - just include the regular JS files, rather than the combined one
982 if(!$successfulWrite) {
983 user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'", E_USER_WARNING);
984 continue;
985 }
986 }
987
988 // @todo Alters the original information, which means you can't call this
989 // method repeatedly - it will behave different on the second call!
990 $this->javascript = $newJSRequirements;
991 $this->css = $newCSSRequirements;
992 }
993
994 function get_custom_scripts() {
995 $requirements = "";
996
997 if($this->customScript) {
998 foreach($this->customScript as $script) {
999 $requirements .= "$script\n";
1000 }
1001 }
1002
1003 return $requirements;
1004 }
1005
1006 /**
1007 * Register the given "themeable stylesheet" as required.
1008 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1009 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
1010 *
1011 * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile"
1012 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1013 */
1014 function themedCSS($name, $media = null) {
1015 global $_CSS_MANIFEST;
1016
1017 $theme = SSViewer::current_theme();
1018
1019 if($theme && isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['themes'])
1020 && isset($_CSS_MANIFEST[$name]['themes'][$theme]))
1021 $this->css($_CSS_MANIFEST[$name]['themes'][$theme], $media);
1022
1023 else if(isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['unthemed'])) $this->css($_CSS_MANIFEST[$name]['unthemed'], $media);
1024 // Normal requirements fails quietly when there is no css - we should do the same
1025 // else user_error("themedCSS - No CSS file '$name.css' found.", E_USER_WARNING);
1026 }
1027
1028 function debug() {
1029 Debug::show($this->javascript);
1030 Debug::show($this->css);
1031 Debug::show($this->customCSS);
1032 Debug::show($this->customScript);
1033 Debug::show($this->customHeadTags);
1034 Debug::show($this->combine_files);
1035 }
1036
1037 }