Version: current

       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 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -