set_combined_files_enabled($enable); } /** * Checks whether combining of css/javascript files is enabled. * @return boolean */ public static function get_combined_files_enabled() { return self::backend()->get_combined_files_enabled(); } /** * Set the relative folder e.g. "assets" for where to store combined files * @param string $folder Path to folder */ public static function set_combined_files_folder($folder) { self::backend()->setCombinedFilesFolder($folder); } /** * Set whether we want to suffix requirements with the time / * location on to the requirements * * @param bool */ public static function set_suffix_requirements($var) { self::backend()->set_suffix_requirements($var); } /** * Return whether we want to suffix requirements * * @return bool */ public static function get_suffix_requirements() { return self::backend()->get_suffix_requirements(); } /** * Instance of requirements for storage * * @var Requirements */ private static $backend = null; public static function backend() { if(!self::$backend) { self::$backend = new Requirements_Backend(); } return self::$backend; } /** * Setter method for changing the Requirements backend * * @param Requirements $backend */ public static function set_backend(Requirements_Backend $backend) { self::$backend = $backend; } /** * Register the given javascript file as required. * * See {@link Requirements_Backend::javascript()} for more info * */ public static function javascript($file) { self::backend()->javascript($file); } /** * Add the javascript code to the header of the page * * See {@link Requirements_Backend::customScript()} for more info * @param script The script content * @param uniquenessID Use this to ensure that pieces of code only get added once. */ public static function customScript($script, $uniquenessID = null) { self::backend()->customScript($script, $uniquenessID); } /** * Include custom CSS styling to the header of the page. * * See {@link Requirements_Backend::customCSS()} * * @param string $script CSS selectors as a string (without \n"; } foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) { $requirements .= "$customHeadTag\n"; } if ($this->force_js_to_bottom) { // Remove all newlines from code to preserve layout $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements); // We put script tags into the body, for performance. // We forcefully put it at the bottom instead of before // the first script-tag occurence $content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content); // Put CSS at the bottom of the head $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); } elseif($this->write_js_to_body) { // Remove all newlines from code to preserve layout $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements); // We put script tags into the body, for performance. // If your template already has script tags in the body, then we try to put our script // tags just before those. Otherwise, we put it at the bottom. $p2 = stripos($content, '
))/U', $content, $commentTags, 0, $p1) && $commentTags[1] == '-->' ); if($canWriteToBody) { $content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1); } else { $content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content); } // Put CSS at the bottom of the head $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); } else { $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); $content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content); } } return $content; } /** * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response */ public function include_in_response(SS_HTTPResponse $response) { $this->process_combined_files(); $jsRequirements = array(); $cssRequirements = array(); foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) { $path = $this->path_for_file($file); if($path) { $jsRequirements[] = str_replace(',', '%2C', $path); } } $response->addHeader('X-Include-JS', implode(',', $jsRequirements)); foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { $path = $this->path_for_file($file); if($path) { $path = str_replace(',', '%2C', $path); $cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path; } } $response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); } /** * Add i18n files from the given javascript directory. SilverStripe expects that the given directory * will contain a number of java script files named by language: en_US.js, de_DE.js, etc. * * @param String The javascript lang directory, relative to the site root, e.g., 'framework/javascript/lang' * @param Boolean Return all relative file paths rather than including them in requirements * @param Boolean Only include language files, not the base libraries */ public function add_i18n_javascript($langDir, $return = false, $langOnly = false) { $files = array(); $base = Director::baseFolder() . '/'; if(i18n::config()->js_i18n) { // Include i18n.js even if no languages are found. The fact that // add_i18n_javascript() was called indicates that the methods in // here are needed. if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js'; if(substr($langDir,-1) != '/') $langDir .= '/'; $candidates = array( 'en.js', 'en_US.js', i18n::get_lang_from_locale(i18n::default_locale()) . '.js', i18n::default_locale() . '.js', i18n::get_lang_from_locale(i18n::get_locale()) . '.js', i18n::get_locale() . '.js', ); foreach($candidates as $candidate) { if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) { $files[] = $langDir . $candidate; } } } else { // Stub i18n implementation for when i18n is disabled. if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js'; } if($return) { return $files; } else { foreach($files as $file) $this->javascript($file); } } /** * Finds the path for specified file. * * @param string $fileOrUrl * @return string|boolean */ protected function path_for_file($fileOrUrl) { if(preg_match('{^//|http[s]?}', $fileOrUrl)) { return $fileOrUrl; } elseif(Director::fileExists($fileOrUrl)) { $filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl); $prefix = Director::baseURL(); $mtimesuffix = ""; $suffix = ''; if($this->suffix_requirements) { $mtimesuffix = "?m=" . filemtime($filePath); $suffix = '&'; } if(strpos($fileOrUrl, '?') !== false) { if (strlen($suffix) == 0) { $suffix = '?'; } $suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1); $fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?')); } else { $suffix = ''; } return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}"; } else { return false; } } /** * Concatenate several css or javascript files into a single dynamically generated * file (stored in {@link Director::baseFolder()}). This increases performance * by fewer HTTP requests. * * The combined file is regenerated * based on every file modification time. Optionally a rebuild can be triggered * by appending ?flush=1 to the URL. * If all files to be combined are javascript, we use the external JSMin library * to minify the javascript. This can be controlled by {@link $combine_js_with_jsmin}. * * All combined files will have a comment on the start of each concatenated file * denoting their original position. For easier debugging, we recommend to only * minify javascript if not in development mode ({@link Director::isDev()}). * * CAUTION: You're responsible for ensuring that the load order for combined files * is retained - otherwise combining javascript files can lead to functional errors * in the javascript logic, and combining css can lead to wrong styling inheritance. * Depending on the javascript logic, you also have to ensure that files are not included * in more than one combine_files() call. * Best practice is to include every javascript file in exactly *one* combine_files() * directive to avoid the issues mentioned above - this is enforced by this function. * * CAUTION: Combining CSS Files discards any "media" information. * * Example for combined JavaScript: *
* Requirements::combine_files(
* 'foobar.js',
* array(
* 'mysite/javascript/foo.js',
* 'mysite/javascript/bar.js',
* )
* );
*
*
* Example for combined CSS:
*
* Requirements::combine_files(
* 'foobar.css',
* array(
* 'mysite/javascript/foo.css',
* 'mysite/javascript/bar.css',
* )
* );
*
*
* @see http://code.google.com/p/jsmin-php/
*
* @todo Should we enforce unique inclusion of files, or leave it to the developer? Can auto-detection cause
* breaks?
*
* @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()}
* by default)
* @param array $files Array of filenames relative to the webroot
* @param string $media Comma-separated list of media-types (e.g. "screen,projector").
*/
public function combine_files($combinedFileName, $files, $media = null) {
// duplicate check
foreach($this->combine_files as $_combinedFileName => $_files) {
$duplicates = array_intersect($_files, $files);
if($duplicates && $combinedFileName != $_combinedFileName) {
user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates)
. " in combined file '{$_combinedFileName}'", E_USER_NOTICE);
return false;
}
}
foreach($files as $index=>$file) {
if(is_array($file)) {
// Either associative array path=>path type=>type or numeric 0=>path 1=>type
// Otherwise, assume path is the first item
if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
switch ($file['type']) {
case 'css':
$this->css($file['path'], $media);
break;
default:
$this->javascript($file['path']);
break;
}
$files[$index] = $file['path'];
} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
switch ($file[1]) {
case 'css':
$this->css($file[0], $media);
break;
default:
$this->javascript($file[0]);
break;
}
$files[$index] = $file[0];
} else {
$file = array_shift($file);
}
}
if (!is_array($file)) {
if(substr($file, -2) == 'js') {
$this->javascript($file);
} elseif(substr($file, -3) == 'css') {
$this->css($file, $media);
} else {
user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
. "please specify by passing using an array instead.", E_USER_NOTICE);
}
}
}
$this->combine_files[$combinedFileName] = $files;
}
/**
* Returns all combined files.
* @return array
*/
public function get_combine_files() {
return $this->combine_files;
}
/**
* Deletes all dynamically generated combined files from the filesystem.
*
* @param string $combinedFileName If left blank, all combined files are deleted.
*/
public function delete_combined_files($combinedFileName = null) {
$combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
$combinedFolder = ($this->getCombinedFilesFolder()) ?
(Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
foreach($combinedFiles as $combinedFile => $sourceItems) {
$filePath = $combinedFolder . '/' . $combinedFile;
if(file_exists($filePath)) {
unlink($filePath);
}
}
}
/**
* Deletes all generated combined files in the configured combined files directory,
* but doesn't delete the directory itself.
*/
public function delete_all_combined_files() {
$combinedFolder = $this->getCombinedFilesFolder();
if(!$combinedFolder) return false;
$path = Director::baseFolder() . '/' . $combinedFolder;
if(file_exists($path)) {
Filesystem::removeFolder($path, true);
}
}
public function clear_combined_files() {
$this->combine_files = array();
}
/**
* See {@link combine_files()}
*
*/
public function process_combined_files() {
// The class_exists call prevents us from loading SapphireTest.php (slow) just to know that
// SapphireTest isn't running :-)
if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
else $runningTest = false;
if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
return;
}
// Make a map of files that could be potentially combined
$combinerCheck = array();
foreach($this->combine_files as $combinedFile => $sourceItems) {
foreach($sourceItems as $sourceItem) {
if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " .
"combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
}
$combinerCheck[$sourceItem] = $combinedFile;
}
}
// Work out the relative URL for the combined files from the base folder
$combinedFilesFolder = ($this->getCombinedFilesFolder()) ? ($this->getCombinedFilesFolder() . '/') : '';
// Figure out which ones apply to this pageview
$combinedFiles = array();
$newJSRequirements = array();
$newCSSRequirements = array();
foreach($this->javascript as $file => $dummy) {
if(isset($combinerCheck[$file])) {
$newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
$combinedFiles[$combinerCheck[$file]] = true;
} else {
$newJSRequirements[$file] = true;
}
}
foreach($this->css as $file => $params) {
if(isset($combinerCheck[$file])) {
// Inherit the parameters from the last file in the combine set.
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
$combinedFiles[$combinerCheck[$file]] = true;
} else {
$newCSSRequirements[$file] = $params;
}
}
// Process the combined files
$base = Director::baseFolder() . '/';
foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) {
$fileList = $this->combine_files[$combinedFile];
$combinedFilePath = $base . $combinedFilesFolder . '/' . $combinedFile;
// Make the folder if necessary
if(!file_exists(dirname($combinedFilePath))) {
Filesystem::makeFolder(dirname($combinedFilePath));
}
// If the file isn't writeable, don't even bother trying to make the combined file and return (falls back
// to uncombined). Complex test because is_writable fails if the file doesn't exist yet.
if((file_exists($combinedFilePath) && !is_writable($combinedFilePath))
|| (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath)))
) {
user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
E_USER_WARNING);
return false;
}
// Determine if we need to build the combined include
if(file_exists($combinedFilePath)) {
// file exists, check modification date of every contained file
$srcLastMod = 0;
foreach($fileList as $file) {
if(file_exists($base . $file)) {
$srcLastMod = max(filemtime($base . $file), $srcLastMod);
}
}
$refresh = $srcLastMod > filemtime($combinedFilePath);
} else {
// file doesn't exist, or refresh was explicitly required
$refresh = true;
}
if(!$refresh) continue;
$combinedData = "";
foreach(array_diff($fileList, $this->blocked) as $file) {
$fileContent = file_get_contents($base . $file);
$fileContent = $this->minifyFile($file, $fileContent);
if ($this->write_header_comment) {
// write a header comment for each file for easier identification and debugging
// also the semicolon between each file is required for jQuery to be combinable properly
$combinedData .= "/****** FILE: $file *****/\n";
}
$combinedData .= $fileContent . "\n";
}
$successfulWrite = false;
$fh = fopen($combinedFilePath, 'wb');
if($fh) {
if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true;
fclose($fh);
unset($fh);
}
// Unsuccessful write - just include the regular JS files, rather than the combined one
if(!$successfulWrite) {
user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
E_USER_WARNING);
continue;
}
}
// @todo Alters the original information, which means you can't call this
// method repeatedly - it will behave different on the second call!
$this->javascript = $newJSRequirements;
$this->css = $newCSSRequirements;
}
protected function minifyFile($filename, $content) {
// if we have a javascript file and jsmin is enabled, minify the content
$isJS = stripos($filename, '.js');
if($isJS && $this->combine_js_with_jsmin) {
require_once('thirdparty/jsmin/jsmin.php');
increase_time_limit_to();
$content = JSMin::minify($content);
}
$content .= ($isJS ? ';' : '') . "\n";
return $content;
}
public function get_custom_scripts() {
$requirements = "";
if($this->customScript) {
foreach($this->customScript as $script) {
$requirements .= "$script\n";
}
}
return $requirements;
}
/**
* @see Requirements::themedCSS()
*/
public function themedCSS($name, $module = null, $media = null) {
$theme = SSViewer::get_theme_folder();
$project = project();
$absbase = BASE_PATH . DIRECTORY_SEPARATOR;
$abstheme = $absbase . $theme;
$absproject = $absbase . $project;
$css = "/css/$name.css";
if(file_exists($absproject . $css)) {
$this->css($project . $css, $media);
} elseif($module && file_exists($abstheme . '_' . $module.$css)) {
$this->css($theme . '_' . $module . $css, $media);
} elseif(file_exists($abstheme . $css)) {
$this->css($theme . $css, $media);
} elseif($module) {
$this->css($module . $css, $media);
}
}
public function debug() {
Debug::show($this->javascript);
Debug::show($this->css);
Debug::show($this->customCSS);
Debug::show($this->customScript);
Debug::show($this->customHeadTags);
Debug::show($this->combine_files);
}
}