Requirements acces to files with query strings (fixes #7735)

Originally authored by florian.thoma, tests added
by Ingo Schommer. Also removed query params from
file paths before calling mtime() on it.

See https://github.com/silverstripe/sapphire/pull/1023
This commit is contained in:
Ingo Schommer 2012-12-18 00:58:09 +01:00
parent a99c829ed1
commit 0242686a7a
2 changed files with 171 additions and 138 deletions

View File

@ -325,6 +325,34 @@ class RequirementsTest extends SapphireTest {
$this->assertContains('</script></body>', $html); $this->assertContains('</script></body>', $html);
} }
public function testSuffix() {
$template = '<html><head></head><body><header>My header</header><p>Body</p></body></html>';
$basePath = $this->getCurrentRelativePath();
$basePath = 'framework' . substr($basePath, strlen(FRAMEWORK_DIR));
$backend = new Requirements_Backend;
$backend->javascript($basePath .'/RequirementsTest_a.js');
$backend->javascript($basePath .'/RequirementsTest_b.js?foo=bar&bla=blubb');
$backend->css($basePath .'/RequirementsTest_a.css');
$backend->css($basePath .'/RequirementsTest_b.css?foo=bar&bla=blubb');
$backend->set_suffix_requirements(true);
$html = $backend->includeInHTML(false, $template);
$this->assertRegexp('/RequirementsTest_a\.js\?m=[\d]*/', $html);
$this->assertRegexp('/RequirementsTest_b\.js\?m=[\d]*&foo=bar&bla=blubb/', $html);
$this->assertRegexp('/RequirementsTest_a\.css\?m=[\d]*/', $html);
$this->assertRegexp('/RequirementsTest_b\.css\?m=[\d]*&foo=bar&bla=blubb/', $html);
$backend->set_suffix_requirements(false);
$html = $backend->includeInHTML(false, $template);
$this->assertNotContains('RequirementsTest_a.js=', $html);
$this->assertNotRegexp('/RequirementsTest_a\.js\?m=[\d]*/', $html);
$this->assertNotRegexp('/RequirementsTest_b\.js\?m=[\d]*&foo=bar&bla=blubb/', $html);
$this->assertNotRegexp('/RequirementsTest_a\.css\?m=[\d]*/', $html);
$this->assertNotRegexp('/RequirementsTest_b\.css\?m=[\d]*&foo=bar&bla=blubb/', $html);
}
public function assertFileIncluded($backend, $type, $files) { public function assertFileIncluded($backend, $type, $files) {
$type = strtolower($type); $type = strtolower($type);
switch (strtolower($type)) { switch (strtolower($type)) {

View File

@ -3,12 +3,12 @@
/** /**
* Requirements tracker, for javascript and css. * Requirements tracker, for javascript and css.
* @todo Document the requirements tracker, and discuss it with the others. * @todo Document the requirements tracker, and discuss it with the others.
* *
* @package framework * @package framework
* @subpackage view * @subpackage view
*/ */
class Requirements { class Requirements {
/** /**
* Enable combining of css/javascript files. * Enable combining of css/javascript files.
* @param boolean $enable * @param boolean $enable
@ -34,38 +34,38 @@ class Requirements {
} }
/** /**
* Set whether we want to suffix requirements with the time / * Set whether we want to suffix requirements with the time /
* location on to the requirements * location on to the requirements
* *
* @param bool * @param bool
*/ */
public static function set_suffix_requirements($var) { public static function set_suffix_requirements($var) {
self::backend()->set_suffix_requirements($var); self::backend()->set_suffix_requirements($var);
} }
/** /**
* Return whether we want to suffix requirements * Return whether we want to suffix requirements
* *
* @return bool * @return bool
*/ */
public static function get_suffix_requirements() { public static function get_suffix_requirements() {
return self::backend()->get_suffix_requirements(); return self::backend()->get_suffix_requirements();
} }
/** /**
* Instance of requirements for storage * Instance of requirements for storage
* *
* @var Requirements * @var Requirements
*/ */
private static $backend = null; private static $backend = null;
public static function backend() { public static function backend() {
if(!self::$backend) { if(!self::$backend) {
self::$backend = new Requirements_Backend(); self::$backend = new Requirements_Backend();
} }
return self::$backend; return self::$backend;
} }
/** /**
* Setter method for changing the Requirements backend * Setter method for changing the Requirements backend
* *
@ -74,20 +74,20 @@ class Requirements {
public static function set_backend(Requirements_Backend $backend) { public static function set_backend(Requirements_Backend $backend) {
self::$backend = $backend; self::$backend = $backend;
} }
/** /**
* Register the given javascript file as required. * Register the given javascript file as required.
* *
* See {@link Requirements_Backend::javascript()} for more info * See {@link Requirements_Backend::javascript()} for more info
* *
*/ */
public static function javascript($file) { public static function javascript($file) {
self::backend()->javascript($file); self::backend()->javascript($file);
} }
/** /**
* Add the javascript code to the header of the page * Add the javascript code to the header of the page
* *
* See {@link Requirements_Backend::customScript()} for more info * See {@link Requirements_Backend::customScript()} for more info
* @param script The script content * @param script The script content
* @param uniquenessID Use this to ensure that pieces of code only get added once. * @param uniquenessID Use this to ensure that pieces of code only get added once.
@ -98,50 +98,50 @@ class Requirements {
/** /**
* Include custom CSS styling to the header of the page. * Include custom CSS styling to the header of the page.
* *
* See {@link Requirements_Backend::customCSS()} * See {@link Requirements_Backend::customCSS()}
* *
* @param string $script CSS selectors as a string (without <style> tag enclosing selectors). * @param string $script CSS selectors as a string (without <style> tag enclosing selectors).
* @param int $uniquenessID Group CSS by a unique ID as to avoid duplicate custom CSS in header * @param int $uniquenessID Group CSS by a unique ID as to avoid duplicate custom CSS in header
*/ */
public static function customCSS($script, $uniquenessID = null) { public static function customCSS($script, $uniquenessID = null) {
self::backend()->customCSS($script, $uniquenessID); self::backend()->customCSS($script, $uniquenessID);
} }
/** /**
* Add the following custom code to the <head> section of the page. * Add the following custom code to the <head> section of the page.
* See {@link Requirements_Backend::insertHeadTags()} * See {@link Requirements_Backend::insertHeadTags()}
* *
* @param string $html * @param string $html
* @param string $uniquenessID * @param string $uniquenessID
*/ */
public static function insertHeadTags($html, $uniquenessID = null) { public static function insertHeadTags($html, $uniquenessID = null) {
self::backend()->insertHeadTags($html, $uniquenessID); self::backend()->insertHeadTags($html, $uniquenessID);
} }
/** /**
* Load the given javascript template with the page. * Load the given javascript template with the page.
* See {@link Requirements_Backend::javascriptTemplate()} * See {@link Requirements_Backend::javascriptTemplate()}
* *
* @param file The template file to load. * @param file The template file to load.
* @param vars The array of variables to load. These variables are loaded via string search & replace. * @param vars The array of variables to load. These variables are loaded via string search & replace.
*/ */
public static function javascriptTemplate($file, $vars, $uniquenessID = null) { public static function javascriptTemplate($file, $vars, $uniquenessID = null) {
self::backend()->javascriptTemplate($file, $vars, $uniquenessID); self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
} }
/** /**
* Register the given stylesheet file as required. * Register the given stylesheet file as required.
* See {@link Requirements_Backend::css()} * See {@link Requirements_Backend::css()}
* *
* @param $file String Filenames should be relative to the base, eg, 'framework/javascript/tree/tree.css' * @param $file String Filenames should be relative to the base, eg, 'framework/javascript/tree/tree.css'
* @param $media String Comma-separated list of media-types (e.g. "screen,projector") * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
* @see http://www.w3.org/TR/REC-CSS2/media.html * @see http://www.w3.org/TR/REC-CSS2/media.html
*/ */
public static function css($file, $media = null) { public static function css($file, $media = null) {
self::backend()->css($file, $media); self::backend()->css($file, $media);
} }
/** /**
* Registers the given themeable stylesheet as required. * Registers the given themeable stylesheet as required.
* *
@ -163,10 +163,10 @@ class Requirements {
/** /**
* Clear either a single or all requirements. * Clear either a single or all requirements.
* Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}. * Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}.
* *
* See {@link Requirements_Backend::clear()} * See {@link Requirements_Backend::clear()}
* *
* @param $file String * @param $file String
*/ */
public static function clear($fileOrID = null) { public static function clear($fileOrID = null) {
@ -186,7 +186,7 @@ class Requirements {
/** /**
* Removes an item from the blocking-list. * Removes an item from the blocking-list.
* See {@link Requirements_Backend::unblock()} * See {@link Requirements_Backend::unblock()}
* *
* @param string $fileOrID * @param string $fileOrID
*/ */
public static function unblock($fileOrID) { public static function unblock($fileOrID) {
@ -200,7 +200,7 @@ class Requirements {
public static function unblock_all() { public static function unblock_all() {
self::backend()->unblock_all(); self::backend()->unblock_all();
} }
/** /**
* Restore requirements cleared by call to Requirements::clear * Restore requirements cleared by call to Requirements::clear
* See {@link Requirements_Backend::restore()} * See {@link Requirements_Backend::restore()}
@ -208,12 +208,12 @@ class Requirements {
public static function restore() { public static function restore() {
self::backend()->restore(); self::backend()->restore();
} }
/** /**
* Update the given HTML content with the appropriate include tags for the registered * Update the given HTML content with the appropriate include tags for the registered
* requirements. * requirements.
* See {@link Requirements_Backend::includeInHTML()} for more information. * See {@link Requirements_Backend::includeInHTML()} for more information.
* *
* @param string $templateFilePath Absolute path for the *.ss template file * @param string $templateFilePath Absolute path for the *.ss template file
* @param string $content HTML content that has already been parsed from the $templateFilePath * @param string $content HTML content that has already been parsed from the $templateFilePath
* through {@link SSViewer}. * through {@link SSViewer}.
@ -222,24 +222,24 @@ class Requirements {
public static function includeInHTML($templateFile, $content) { public static function includeInHTML($templateFile, $content) {
return self::backend()->includeInHTML($templateFile, $content); return self::backend()->includeInHTML($templateFile, $content);
} }
public static function include_in_response(SS_HTTPResponse $response) { public static function include_in_response(SS_HTTPResponse $response) {
return self::backend()->include_in_response($response); return self::backend()->include_in_response($response);
} }
/** /**
* Add i18n files from the given javascript directory. * Add i18n files from the given javascript directory.
* *
* @param String * @param String
* @param Boolean * @param Boolean
* @param Boolean * @param Boolean
* *
* See {@link Requirements_Backend::add_i18n_javascript()} for more information. * See {@link Requirements_Backend::add_i18n_javascript()} for more information.
*/ */
public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) { public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
return self::backend()->add_i18n_javascript($langDir, $return, $langOnly); return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
} }
/** /**
* Concatenate several css or javascript files into a single dynamically generated file. * Concatenate several css or javascript files into a single dynamically generated file.
* See {@link Requirements_Backend::combine_files()} for more info. * See {@link Requirements_Backend::combine_files()} for more info.
@ -250,27 +250,27 @@ class Requirements {
public static function combine_files($combinedFileName, $files) { public static function combine_files($combinedFileName, $files) {
self::backend()->combine_files($combinedFileName, $files); self::backend()->combine_files($combinedFileName, $files);
} }
/** /**
* Returns all combined files. * Returns all combined files.
* See {@link Requirements_Backend::get_combine_files()} * See {@link Requirements_Backend::get_combine_files()}
* *
* @return array * @return array
*/ */
public static function get_combine_files() { public static function get_combine_files() {
return self::backend()->get_combine_files(); return self::backend()->get_combine_files();
} }
/** /**
* Deletes all dynamically generated combined files from the filesystem. * Deletes all dynamically generated combined files from the filesystem.
* See {@link Requirements_Backend::delete_combine_files()} * See {@link Requirements_Backend::delete_combine_files()}
* *
* @param string $combinedFileName If left blank, all combined files are deleted. * @param string $combinedFileName If left blank, all combined files are deleted.
*/ */
public static function delete_combined_files($combinedFileName = null) { public static function delete_combined_files($combinedFileName = null) {
return self::backend()->delete_combined_files($combinedFileName); return self::backend()->delete_combined_files($combinedFileName);
} }
/** /**
* Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()} * Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
@ -278,7 +278,7 @@ class Requirements {
public static function clear_combined_files() { public static function clear_combined_files() {
self::backend()->clear_combined_files(); self::backend()->clear_combined_files();
} }
/** /**
* See {@link combine_files()}. * See {@link combine_files()}.
*/ */
@ -295,18 +295,18 @@ class Requirements {
public static function get_custom_scripts() { public static function get_custom_scripts() {
return self::backend()->get_custom_scripts(); return self::backend()->get_custom_scripts();
} }
/** /**
* Set whether you want to write the JS to the body of the page or * Set whether you want to write the JS to the body of the page or
* in the head section * in the head section
* *
* @see Requirements_Backend::set_write_js_to_body() * @see Requirements_Backend::set_write_js_to_body()
* @param boolean * @param boolean
*/ */
public static function set_write_js_to_body($var) { public static function set_write_js_to_body($var) {
self::backend()->set_write_js_to_body($var); self::backend()->set_write_js_to_body($var);
} }
public static function debug() { public static function debug() {
return self::backend()->debug(); return self::backend()->debug();
} }
@ -431,7 +431,7 @@ class Requirements_Backend {
public function set_combined_files_enabled($enable) { public function set_combined_files_enabled($enable) {
$this->combined_files_enabled = (bool) $enable; $this->combined_files_enabled = (bool) $enable;
} }
public function get_combined_files_enabled() { public function get_combined_files_enabled() {
return $this->combined_files_enabled; return $this->combined_files_enabled;
} }
@ -442,33 +442,33 @@ class Requirements_Backend {
public function setCombinedFilesFolder($folder) { public function setCombinedFilesFolder($folder) {
$this->combinedFilesFolder = $folder; $this->combinedFilesFolder = $folder;
} }
/** /**
* @return String Folder relative to the webroot * @return String Folder relative to the webroot
*/ */
public function getCombinedFilesFolder() { public function getCombinedFilesFolder() {
return ($this->combinedFilesFolder) ? $this->combinedFilesFolder : ASSETS_DIR . '/_combinedfiles'; return ($this->combinedFilesFolder) ? $this->combinedFilesFolder : ASSETS_DIR . '/_combinedfiles';
} }
/** /**
* Set whether we want to suffix requirements with the time / * Set whether we want to suffix requirements with the time /
* location on to the requirements * location on to the requirements
* *
* @param bool * @param bool
*/ */
public function set_suffix_requirements($var) { public function set_suffix_requirements($var) {
$this->suffix_requirements = $var; $this->suffix_requirements = $var;
} }
/** /**
* Return whether we want to suffix requirements * Return whether we want to suffix requirements
* *
* @return bool * @return bool
*/ */
public function get_suffix_requirements() { public function get_suffix_requirements() {
return $this->suffix_requirements; return $this->suffix_requirements;
} }
/** /**
* Set whether you want the files written to the head or the body. It * Set whether you want the files written to the head or the body. It
* writes to the body by default which can break some scripts * writes to the body by default which can break some scripts
@ -482,11 +482,11 @@ class Requirements_Backend {
* Register the given javascript file as required. * Register the given javascript file as required.
* Filenames should be relative to the base, eg, 'framework/javascript/loader.js' * Filenames should be relative to the base, eg, 'framework/javascript/loader.js'
*/ */
public function javascript($file) { public function javascript($file) {
$this->javascript[$file] = true; $this->javascript[$file] = true;
} }
/** /**
* Returns an array of all included javascript * Returns an array of all included javascript
* *
@ -495,7 +495,7 @@ class Requirements_Backend {
public function get_javascript() { public function get_javascript() {
return array_keys(array_diff_key($this->javascript,$this->blocked)); return array_keys(array_diff_key($this->javascript,$this->blocked));
} }
/** /**
* Add the javascript code to the header of the page * Add the javascript code to the header of the page
* @todo Make Requirements automatically put this into a separate file :-) * @todo Make Requirements automatically put this into a separate file :-)
@ -505,10 +505,10 @@ class Requirements_Backend {
public function customScript($script, $uniquenessID = null) { public function customScript($script, $uniquenessID = null) {
if($uniquenessID) $this->customScript[$uniquenessID] = $script; if($uniquenessID) $this->customScript[$uniquenessID] = $script;
else $this->customScript[] = $script; else $this->customScript[] = $script;
$script .= "\n"; $script .= "\n";
} }
/** /**
* Include custom CSS styling to the header of the page. * Include custom CSS styling to the header of the page.
* *
@ -519,7 +519,7 @@ class Requirements_Backend {
if($uniquenessID) $this->customCSS[$uniquenessID] = $script; if($uniquenessID) $this->customCSS[$uniquenessID] = $script;
else $this->customCSS[] = $script; else $this->customCSS[] = $script;
} }
/** /**
* Add the following custom code to the <head> section of the page. * Add the following custom code to the <head> section of the page.
* *
@ -530,7 +530,7 @@ class Requirements_Backend {
if($uniquenessID) $this->customHeadTags[$uniquenessID] = $html; if($uniquenessID) $this->customHeadTags[$uniquenessID] = $html;
else $this->customHeadTags[] = $html; else $this->customHeadTags[] = $html;
} }
/** /**
* Load the given javascript template with the page. * Load the given javascript template with the page.
* @param file The template file to load. * @param file The template file to load.
@ -545,16 +545,16 @@ class Requirements_Backend {
$search[] = '$' . $k; $search[] = '$' . $k;
$replace[] = str_replace("\\'","'", Convert::raw2js($v)); $replace[] = str_replace("\\'","'", Convert::raw2js($v));
} }
$script = str_replace($search, $replace, $script); $script = str_replace($search, $replace, $script);
$this->customScript($script, $uniquenessID); $this->customScript($script, $uniquenessID);
} }
/** /**
* Register the given stylesheet file as required. * Register the given stylesheet file as required.
* *
* @param $file String Filenames should be relative to the base, eg, 'framework/javascript/tree/tree.css' * @param $file String Filenames should be relative to the base, eg, 'framework/javascript/tree/tree.css'
* @param $media String Comma-separated list of media-types (e.g. "screen,projector") * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
* @see http://www.w3.org/TR/REC-CSS2/media.html * @see http://www.w3.org/TR/REC-CSS2/media.html
*/ */
public function css($file, $media = null) { public function css($file, $media = null) {
@ -562,28 +562,28 @@ class Requirements_Backend {
"media" => $media "media" => $media
); );
} }
public function get_css() { public function get_css() {
return array_diff_key($this->css, $this->blocked); return array_diff_key($this->css, $this->blocked);
} }
/** /**
* Needed to actively prevent the inclusion of a file, * Needed to actively prevent the inclusion of a file,
* e.g. when using your own prototype.js. * e.g. when using your own prototype.js.
* Blocking should only be used as an exception, because * Blocking should only be used as an exception, because
* it is hard to trace back. You can just block items with an * it is hard to trace back. You can just block items with an
* ID, so make sure you add an unique identifier to customCSS() and customScript(). * ID, so make sure you add an unique identifier to customCSS() and customScript().
* *
* @param string $fileOrID * @param string $fileOrID
*/ */
public function block($fileOrID) { public function block($fileOrID) {
$this->blocked[$fileOrID] = $fileOrID; $this->blocked[$fileOrID] = $fileOrID;
} }
/** /**
* Clear either a single or all requirements. * Clear either a single or all requirements.
* Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}. * Caution: Clearing single rules works only with customCSS and customScript if you specified a {@uniquenessID}.
* *
* @param $file String * @param $file String
*/ */
public function clear($fileOrID = null) { public function clear($fileOrID = null) {
@ -600,7 +600,7 @@ class Requirements_Backend {
$this->disabled['customScript'] = $this->customScript; $this->disabled['customScript'] = $this->customScript;
$this->disabled['customCSS'] = $this->customCSS; $this->disabled['customCSS'] = $this->customCSS;
$this->disabled['customHeadTags'] = $this->customHeadTags; $this->disabled['customHeadTags'] = $this->customHeadTags;
$this->javascript = array(); $this->javascript = array();
$this->css = array(); $this->css = array();
$this->customScript = array(); $this->customScript = array();
@ -608,7 +608,7 @@ class Requirements_Backend {
$this->customHeadTags = array(); $this->customHeadTags = array();
} }
} }
/** /**
* Removes an item from the blocking-list. * Removes an item from the blocking-list.
* CAUTION: Does not "re-add" any previously blocked elements. * CAUTION: Does not "re-add" any previously blocked elements.
@ -623,7 +623,7 @@ class Requirements_Backend {
public function unblock_all() { public function unblock_all() {
$this->blocked = array(); $this->blocked = array();
} }
/** /**
* Restore requirements cleared by call to Requirements::clear * Restore requirements cleared by call to Requirements::clear
*/ */
@ -634,14 +634,14 @@ class Requirements_Backend {
$this->customCSS = $this->disabled['customCSS']; $this->customCSS = $this->disabled['customCSS'];
$this->customHeadTags = $this->disabled['customHeadTags']; $this->customHeadTags = $this->disabled['customHeadTags'];
} }
/** /**
* Update the given HTML content with the appropriate include tags for the registered * Update the given HTML content with the appropriate include tags for the registered
* requirements. Needs to receive a valid HTML/XHTML template in the $content parameter, * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
* including a <head> tag. The requirements will insert before the closing <head> tag automatically. * including a <head> tag. The requirements will insert before the closing <head> tag automatically.
* *
* @todo Calculate $prefix properly * @todo Calculate $prefix properly
* *
* @param string $templateFilePath Absolute path for the *.ss template file * @param string $templateFilePath Absolute path for the *.ss template file
* @param string $content HTML content that has already been parsed from the $templateFilePath * @param string $content HTML content that has already been parsed from the $templateFilePath
* through {@link SSViewer}. * through {@link SSViewer}.
@ -654,50 +654,50 @@ class Requirements_Backend {
&& ($this->css||$this->javascript||$this->customCSS||$this->customScript||$this->customHeadTags)) { && ($this->css||$this->javascript||$this->customCSS||$this->customScript||$this->customHeadTags)) {
$requirements = ''; $requirements = '';
$jsRequirements = ''; $jsRequirements = '';
// Combine files - updates $this->javascript and $this->css // Combine files - updates $this->javascript and $this->css
$this->process_combined_files(); $this->process_combined_files();
foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) { foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) {
$path = $this->path_for_file($file); $path = $this->path_for_file($file);
if($path) { if($path) {
$jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n"; $jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n";
} }
} }
// add all inline javascript *after* including external files which // add all inline javascript *after* including external files which
// they might rely on // they might rely on
if($this->customScript) { if($this->customScript) {
foreach(array_diff_key($this->customScript,$this->blocked) as $script) { foreach(array_diff_key($this->customScript,$this->blocked) as $script) {
$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n"; $jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
$jsRequirements .= "$script\n"; $jsRequirements .= "$script\n";
$jsRequirements .= "\n//]]>\n</script>\n"; $jsRequirements .= "\n//]]>\n</script>\n";
} }
} }
foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
$path = $this->path_for_file($file); $path = $this->path_for_file($file);
if($path) { if($path) {
$media = (isset($params['media']) && !empty($params['media'])) $media = (isset($params['media']) && !empty($params['media']))
? " media=\"{$params['media']}\"" : ""; ? " media=\"{$params['media']}\"" : "";
$requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n"; $requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n";
} }
} }
foreach(array_diff_key($this->customCSS, $this->blocked) as $css) { foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
$requirements .= "<style type=\"text/css\">\n$css\n</style>\n"; $requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
} }
foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) { foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
$requirements .= "$customHeadTag\n"; $requirements .= "$customHeadTag\n";
} }
if($this->write_js_to_body) { if($this->write_js_to_body) {
// Remove all newlines from code to preserve layout // Remove all newlines from code to preserve layout
$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements); $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
// We put script tags into the body, for performance. // We put script tags into the body, for performance.
// If your template already has script tags in the body, then we put our script // 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. // tags just before those. Otherwise, we put it at the bottom.
$p1 = strripos($content, '<script'); $p1 = strripos($content, '<script');
$p2 = stripos($content, '<body'); $p2 = stripos($content, '<body');
@ -706,15 +706,15 @@ class Requirements_Backend {
} else { } else {
$content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content); $content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content);
} }
// Put CSS at the bottom of the head // Put CSS at the bottom of the head
$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
} else { } else {
$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
$content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content); $content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content);
} }
} }
if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML"); if(isset($_GET['debug_profile'])) Profiler::unmark("Requirements::includeInHTML");
return $content; return $content;
@ -724,20 +724,20 @@ class Requirements_Backend {
* Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the HTTP response
*/ */
public function include_in_response(SS_HTTPResponse $response) { public function include_in_response(SS_HTTPResponse $response) {
$this->process_combined_files(); $this->process_combined_files();
$jsRequirements = array(); $jsRequirements = array();
$cssRequirements = array(); $cssRequirements = array();
foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) { foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) {
$path = $this->path_for_file($file); $path = $this->path_for_file($file);
if($path) { if($path) {
$jsRequirements[] = str_replace(',', '%2C', $path); $jsRequirements[] = str_replace(',', '%2C', $path);
} }
} }
$response->addHeader('X-Include-JS', implode(',', $jsRequirements)); $response->addHeader('X-Include-JS', implode(',', $jsRequirements));
foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
$path = $this->path_for_file($file); $path = $this->path_for_file($file);
if($path) { if($path) {
$path = str_replace(',', '%2C', $path); $path = str_replace(',', '%2C', $path);
@ -747,11 +747,11 @@ class Requirements_Backend {
$response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); $response->addHeader('X-Include-CSS', implode(',', $cssRequirements));
} }
/** /**
* Add i18n files from the given javascript directory. SilverStripe expects that the given directory * 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. * 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 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 Return all relative file paths rather than including them in requirements
* @param Boolean Only include language files, not the base libraries * @param Boolean Only include language files, not the base libraries
@ -765,10 +765,10 @@ class Requirements_Backend {
if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js'; if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js';
if(substr($langDir,-1) != '/') $langDir .= '/'; if(substr($langDir,-1) != '/') $langDir .= '/';
$files[] = $langDir . i18n::default_locale() . '.js'; $files[] = $langDir . i18n::default_locale() . '.js';
$files[] = $langDir . i18n::get_locale() . '.js'; $files[] = $langDir . i18n::get_locale() . '.js';
// Stub i18n implementation for when i18n is disabled. // Stub i18n implementation for when i18n is disabled.
} else { } else {
if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js'; if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js';
@ -779,49 +779,54 @@ class Requirements_Backend {
} else { } else {
foreach($files as $file) $this->javascript($file); foreach($files as $file) $this->javascript($file);
} }
} }
/** /**
* Finds the path for specified file. * Finds the path for specified file.
* *
* @param string $fileOrUrl * @param string $fileOrUrl
* @return string|boolean * @return string|boolean
*/ */
protected function path_for_file($fileOrUrl) { protected function path_for_file($fileOrUrl) {
if(preg_match('{^//|http[s]?}', $fileOrUrl)) { if(preg_match('{^//|http[s]?}', $fileOrUrl)) {
return $fileOrUrl; return $fileOrUrl;
} elseif(Director::fileExists($fileOrUrl)) { } elseif(Director::fileExists($fileOrUrl)) {
$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
$prefix = Director::baseURL(); $prefix = Director::baseURL();
$mtimesuffix = ""; $mtimesuffix = "";
$suffix = ''; $suffix = '';
if(strpos($fileOrUrl, '?') !== false) {
$suffix = '&' . substr($fileOrUrl, strpos($fileOrUrl, '?')+1);
$fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?'));
}
if($this->suffix_requirements) { if($this->suffix_requirements) {
$mtimesuffix = "?m=" . filemtime(Director::baseFolder() . '/' . $fileOrUrl); $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, '?'));
} }
return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}"; return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}";
} else { } else {
return false; return false;
} }
} }
/** /**
* Concatenate several css or javascript files into a single dynamically generated * Concatenate several css or javascript files into a single dynamically generated
* file (stored in {@link Director::baseFolder()}). This increases performance * file (stored in {@link Director::baseFolder()}). This increases performance
* by fewer HTTP requests. * by fewer HTTP requests.
* *
* The combined file is regenerated * The combined file is regenerated
* based on every file modification time. Optionally a rebuild can be triggered * based on every file modification time. Optionally a rebuild can be triggered
* by appending ?flush=1 to the URL. * by appending ?flush=1 to the URL.
* If all files to be combined are javascript, we use the external JSMin library * 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}. * 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 * 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 * denoting their original position. For easier debugging, we recommend to only
* minify javascript if not in development mode ({@link Director::isDev()}). * minify javascript if not in development mode ({@link Director::isDev()}).
* *
* CAUTION: You're responsible for ensuring that the load order for combined files * CAUTION: You're responsible for ensuring that the load order for combined files
* is retained - otherwise combining javascript files can lead to functional errors * is retained - otherwise combining javascript files can lead to functional errors
* in the javascript logic, and combining css can lead to wrong styling inheritance. * in the javascript logic, and combining css can lead to wrong styling inheritance.
@ -829,7 +834,7 @@ class Requirements_Backend {
* in more than one combine_files() call. * in more than one combine_files() call.
* Best practice is to include every javascript file in exactly *one* combine_files() * 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. * directive to avoid the issues mentioned above - this is enforced by this function.
* *
* CAUTION: Combining CSS Files discards any "media" information. * CAUTION: Combining CSS Files discards any "media" information.
* *
* Example for combined JavaScript: * Example for combined JavaScript:
@ -855,10 +860,10 @@ class Requirements_Backend {
* </code> * </code>
* *
* @see http://code.google.com/p/jsmin-php/ * @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 * @todo Should we enforce unique inclusion of files, or leave it to the developer? Can auto-detection cause
* breaks? * breaks?
* *
* @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()} * @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()}
* by default) * by default)
* @param array $files Array of filenames relative to the webroot * @param array $files Array of filenames relative to the webroot
@ -914,7 +919,7 @@ class Requirements_Backend {
} }
$this->combine_files[$combinedFileName] = $files; $this->combine_files[$combinedFileName] = $files;
} }
/** /**
* Returns all combined files. * Returns all combined files.
* @return array * @return array
@ -922,15 +927,15 @@ class Requirements_Backend {
public function get_combine_files() { public function get_combine_files() {
return $this->combine_files; return $this->combine_files;
} }
/** /**
* Deletes all dynamically generated combined files from the filesystem. * Deletes all dynamically generated combined files from the filesystem.
* *
* @param string $combinedFileName If left blank, all combined files are deleted. * @param string $combinedFileName If left blank, all combined files are deleted.
*/ */
public function delete_combined_files($combinedFileName = null) { public function delete_combined_files($combinedFileName = null) {
$combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files; $combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
$combinedFolder = ($this->getCombinedFilesFolder()) ? $combinedFolder = ($this->getCombinedFilesFolder()) ?
(Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder(); (Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
foreach($combinedFiles as $combinedFile => $sourceItems) { foreach($combinedFiles as $combinedFile => $sourceItems) {
$filePath = $combinedFolder . '/' . $combinedFile; $filePath = $combinedFolder . '/' . $combinedFile;
@ -939,7 +944,7 @@ class Requirements_Backend {
} }
} }
} }
public function clear_combined_files() { public function clear_combined_files() {
$this->combine_files = array(); $this->combine_files = array();
} }
@ -953,7 +958,7 @@ class Requirements_Backend {
// SapphireTest isn't running :-) // SapphireTest isn't running :-)
if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test(); if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
else $runningTest = false; else $runningTest = false;
if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) { if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
return; return;
} }
@ -962,12 +967,12 @@ class Requirements_Backend {
$combinerCheck = array(); $combinerCheck = array();
foreach($this->combine_files as $combinedFile => $sourceItems) { foreach($this->combine_files as $combinedFile => $sourceItems) {
foreach($sourceItems as $sourceItem) { foreach($sourceItems as $sourceItem) {
if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){ if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " . user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " .
"combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING); "combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
} }
$combinerCheck[$sourceItem] = $combinedFile; $combinerCheck[$sourceItem] = $combinedFile;
} }
} }
@ -986,7 +991,7 @@ class Requirements_Backend {
$newJSRequirements[$file] = true; $newJSRequirements[$file] = true;
} }
} }
foreach($this->css as $file => $params) { foreach($this->css as $file => $params) {
if(isset($combinerCheck[$file])) { if(isset($combinerCheck[$file])) {
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true; $newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
@ -1042,7 +1047,7 @@ class Requirements_Backend {
$isJS = stripos($file, '.js'); $isJS = stripos($file, '.js');
if($isJS && $this->combine_js_with_jsmin) { if($isJS && $this->combine_js_with_jsmin) {
require_once('thirdparty/jsmin/jsmin.php'); require_once('thirdparty/jsmin/jsmin.php');
increase_time_limit_to(); increase_time_limit_to();
$fileContent = JSMin::minify($fileContent); $fileContent = JSMin::minify($fileContent);
} }
@ -1075,13 +1080,13 @@ class Requirements_Backend {
public function get_custom_scripts() { public function get_custom_scripts() {
$requirements = ""; $requirements = "";
if($this->customScript) { if($this->customScript) {
foreach($this->customScript as $script) { foreach($this->customScript as $script) {
$requirements .= "$script\n"; $requirements .= "$script\n";
} }
} }
return $requirements; return $requirements;
} }
@ -1103,7 +1108,7 @@ class Requirements_Backend {
$this->css($module.$css, $media); $this->css($module.$css, $media);
} }
} }
public function debug() { public function debug() {
Debug::show($this->javascript); Debug::show($this->javascript);
Debug::show($this->css); Debug::show($this->css);
@ -1112,5 +1117,5 @@ class Requirements_Backend {
Debug::show($this->customHeadTags); Debug::show($this->customHeadTags);
Debug::show($this->combine_files); Debug::show($this->combine_files);
} }
} }