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 * */ 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. */ 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->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 put our script // tags just before those. Otherwise, we put it at the bottom. $p1 = strripos($content, ' $p2) { $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); } } if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML"); return $content; } /** * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response */ 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[] = $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) $cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path; } $response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); } /** * Add i18n files from the given javascript directory. Sapphire expects that the given directory * will contain a number of java script files named by language: en_US.js, de_DE.js, etc. * @param $langDir The javascript lang directory, relative to the site root, e.g., 'sapphire/javascript/lang' */ public function add_i18n_javascript($langDir) { if(i18n::get_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. $this->javascript(SAPPHIRE_DIR . '/javascript/i18n.js'); if(substr($langDir,-1) != '/') $langDir .= '/'; $this->javascript($langDir . i18n::default_locale() . '.js'); $this->javascript($langDir . i18n::get_locale() . '.js'); // Stub i18n implementation for when i18n is disabled. } else { $this->javascript[SAPPHIRE_DIR . '/javascript/i18nx.js'] = true; } } /** * 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)) { $prefix = Director::baseURL(); $mtimesuffix = ""; $suffix = ''; if(strpos($fileOrUrl, '?') !== false) { $suffix = '&' . substr($fileOrUrl, strpos($fileOrUrl, '?')+1); $fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?')); } if($this->suffix_requirements) { $mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' . $fileOrUrl); } 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 */ function combine_files($combinedFileName, $files) { // 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']) && ($file['type'] == 'css' || $file['type'] == 'javascript' || $file['type'] == 'js')) { switch ($file['type']) { case 'css': $this->css($file['path']); break; default: $this->javascript($file['path']); break; } $files[$index] = $file['path']; } elseif (isset($file[1]) && ($file[1] == 'css' || $file[1] == 'javascript' || $file[1] == 'js')) { switch ($file[1]) { case 'css': $this->css($file[0]); 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); } 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 */ 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. */ 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); } } } function clear_combined_files() { $this->combine_files = array(); } /** * See {@link combine_files()} * */ 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) || !$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])) { $newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true; $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 writebale, don't even bother trying to make the combined file // 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); continue; } // Determine if we need to build the combined include if(file_exists($combinedFilePath) && !isset($_GET['flush'])) { // file exists, check modification date of every contained file $srcLastMod = 0; foreach($fileList as $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); // if we have a javascript file and jsmin is enabled, minify the content $isJS = stripos($file, '.js'); if($isJS && $this->combine_js_with_jsmin) { require_once('thirdparty/jsmin/jsmin.php'); increase_time_limit_to(); $fileContent = JSMin::minify($fileContent); } // 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" . $fileContent . "\n".($isJS ? ';' : '')."\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; } function get_custom_scripts() { $requirements = ""; if($this->customScript) { foreach($this->customScript as $script) { $requirements .= "$script\n"; } } return $requirements; } /** * Register the given "themeable stylesheet" as required. * Themeable stylesheets have globally unique names, just like templates and PHP files. * Because of this, they can be replaced by similarly named CSS files in the theme directory. * * @param $name String The identifier of the file. For example, css/MyFile.css would have the identifier "MyFile" * @param $media String Comma-separated list of media-types (e.g. "screen,projector") */ function themedCSS($name, $media = null) { global $_CSS_MANIFEST; $theme = SSViewer::current_theme(); if($theme && isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['themes']) && isset($_CSS_MANIFEST[$name]['themes'][$theme])) $this->css($_CSS_MANIFEST[$name]['themes'][$theme], $media); else if(isset($_CSS_MANIFEST[$name]) && isset($_CSS_MANIFEST[$name]['unthemed'])) $this->css($_CSS_MANIFEST[$name]['unthemed'], $media); // Normal requirements fails quietly when there is no css - we should do the same // else user_error("themedCSS - No CSS file '$name.css' found.", E_USER_WARNING); } 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); } }