mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
4ca3d1dc94
Fixes #5829
1892 lines
56 KiB
PHP
1892 lines
56 KiB
PHP
<?php
|
|
|
|
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
|
|
use SilverStripe\View\ThemeResourceLoader;
|
|
|
|
/**
|
|
* Requirements tracker for JavaScript and CSS.
|
|
*
|
|
* @package framework
|
|
* @subpackage view
|
|
*/
|
|
class Requirements implements Flushable {
|
|
|
|
/**
|
|
* Flag whether combined files should be deleted on flush.
|
|
*
|
|
* By default all combined files are deleted on flush. If combined files are stored in source control,
|
|
* and thus updated manually, you might want to turn this on to disable this behaviour.
|
|
*
|
|
* @config
|
|
* @var bool
|
|
*/
|
|
private static $disable_flush_combined = false;
|
|
|
|
/**
|
|
* Triggered early in the request when a flush is requested
|
|
*/
|
|
public static function flush() {
|
|
$disabled = Config::inst()->get(static::class, 'disable_flush_combined');
|
|
if(!$disabled) {
|
|
self::delete_all_combined_files();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable combining of css/javascript files.
|
|
*
|
|
* @param bool $enable
|
|
*/
|
|
public static function set_combined_files_enabled($enable) {
|
|
self::backend()->setCombinedFilesEnabled($enable);
|
|
}
|
|
|
|
/**
|
|
* Checks whether combining of css/javascript files is enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_combined_files_enabled() {
|
|
return self::backend()->getCombinedFilesEnabled();
|
|
}
|
|
|
|
/**
|
|
* 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 to add caching query params to the requests for file-based requirements.
|
|
* Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
|
|
* filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
|
|
* while automatically busting this cache every time the file is changed.
|
|
*
|
|
* @param bool
|
|
*/
|
|
public static function set_suffix_requirements($var) {
|
|
self::backend()->setSuffixRequirements($var);
|
|
}
|
|
|
|
/**
|
|
* Check whether we want to suffix requirements
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_suffix_requirements() {
|
|
return self::backend()->getSuffixRequirements();
|
|
}
|
|
|
|
/**
|
|
* Instance of the requirements for storage. You can create your own backend to change the
|
|
* default JS and CSS inclusion behaviour.
|
|
*
|
|
* @var Requirements_Backend
|
|
*/
|
|
private static $backend = null;
|
|
|
|
/**
|
|
* @return Requirements_Backend
|
|
*/
|
|
public static function backend() {
|
|
if(!self::$backend) {
|
|
self::$backend = Injector::inst()->create('Requirements_Backend');
|
|
}
|
|
return self::$backend;
|
|
}
|
|
|
|
/**
|
|
* Setter method for changing the Requirements backend
|
|
*
|
|
* @param Requirements_Backend $backend
|
|
*/
|
|
public static function set_backend(Requirements_Backend $backend) {
|
|
self::$backend = $backend;
|
|
}
|
|
|
|
/**
|
|
* Register the given JavaScript file as required.
|
|
*
|
|
* @param string $file Relative to docroot
|
|
* @param array $options List of options. Available options include:
|
|
* - 'provides' : List of scripts files included in this file
|
|
* - 'async' : Boolean value to set async attribute to script tag
|
|
* - 'defer' : Boolean value to set defer attribute to script tag
|
|
*/
|
|
public static function javascript($file, $options = array()) {
|
|
self::backend()->javascript($file, $options);
|
|
}
|
|
|
|
/**
|
|
* Register the given JavaScript code into the list of requirements
|
|
*
|
|
* @param string $script The script content as a string (without enclosing <script> tag)
|
|
* @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public static function customScript($script, $uniquenessID = null) {
|
|
self::backend()->customScript($script, $uniquenessID);
|
|
}
|
|
|
|
/**
|
|
* Return all registered custom scripts
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_custom_scripts() {
|
|
return self::backend()->getCustomScripts();
|
|
}
|
|
|
|
/**
|
|
* Register the given CSS styles into the list of requirements
|
|
*
|
|
* @param string $script CSS selectors as a string (without enclosing <style> tag)
|
|
* @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public static function customCSS($script, $uniquenessID = null) {
|
|
self::backend()->customCSS($script, $uniquenessID);
|
|
}
|
|
|
|
/**
|
|
* Add the following custom HTML code to the <head> section of the page
|
|
*
|
|
* @param string $html Custom HTML code
|
|
* @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public static function insertHeadTags($html, $uniquenessID = null) {
|
|
self::backend()->insertHeadTags($html, $uniquenessID);
|
|
}
|
|
|
|
/**
|
|
* Include the content of the given JavaScript file in the list of requirements. Dollar-sign
|
|
* variables will be interpolated with values from $vars similar to a .ss template.
|
|
*
|
|
* @param string $file The template file to load, relative to docroot
|
|
* @param string[]|int[] $vars The array of variables to interpolate.
|
|
* @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public static function javascriptTemplate($file, $vars, $uniquenessID = null) {
|
|
self::backend()->javascriptTemplate($file, $vars, $uniquenessID);
|
|
}
|
|
|
|
/**
|
|
* Register the given stylesheet into the list of requirements.
|
|
*
|
|
* @param string $file The CSS file to load, relative to site root
|
|
* @param string $media Comma-separated list of media types to use in the link tag
|
|
* (e.g. 'screen,projector')
|
|
*/
|
|
public static function css($file, $media = null) {
|
|
self::backend()->css($file, $media);
|
|
}
|
|
|
|
/**
|
|
* Registers the given themeable stylesheet as required.
|
|
*
|
|
* A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
|
|
* and it that doesn't exist and the module parameter is set then a CSS file with that name in
|
|
* the module is used.
|
|
*
|
|
* @param string $name The name of the file - eg '/css/File.css' would have the name 'File'
|
|
* @param string $media Comma-separated list of media types to use in the link tag
|
|
* (e.g. 'screen,projector')
|
|
*/
|
|
public static function themedCSS($name, $media = null) {
|
|
self::backend()->themedCSS($name, $media);
|
|
}
|
|
|
|
/**
|
|
* Registers the given themeable javascript as required.
|
|
*
|
|
* A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
|
|
* and it that doesn't exist and the module parameter is set then a javascript file with that name in
|
|
* the module is used.
|
|
*
|
|
* @param string $name The name of the file - eg '/javascript/File.js' would have the name 'File'
|
|
* @param string $type Comma-separated list of types to use in the script tag
|
|
* (e.g. 'text/javascript,text/ecmascript')
|
|
*/
|
|
public static function themedJavascript($name, $type = null) {
|
|
return self::backend()->themedJavascript($name, $type);
|
|
}
|
|
|
|
/**
|
|
* Clear either a single or all requirements
|
|
*
|
|
* Caution: Clearing single rules added via customCSS and customScript only works if you
|
|
* originally specified a $uniquenessID.
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public static function clear($fileOrID = null) {
|
|
self::backend()->clear($fileOrID);
|
|
}
|
|
|
|
/**
|
|
* Restore requirements cleared by call to Requirements::clear
|
|
*/
|
|
public static function restore() {
|
|
self::backend()->restore();
|
|
}
|
|
|
|
/**
|
|
* Block inclusion of a specific file
|
|
*
|
|
* The difference between this and {@link clear} is that the calling order does not matter;
|
|
* {@link clear} must be called after the initial registration, whereas {@link block} can be
|
|
* used in advance. This is useful, for example, to block scripts included by a superclass
|
|
* without having to override entire functions and duplicate a lot of code.
|
|
*
|
|
* Note that blocking should be used sparingly because it's hard to trace where an file is
|
|
* being blocked from.
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public static function block($fileOrID) {
|
|
self::backend()->block($fileOrID);
|
|
}
|
|
|
|
/**
|
|
* Remove an item from the block list
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public static function unblock($fileOrID) {
|
|
self::backend()->unblock($fileOrID);
|
|
}
|
|
|
|
/**
|
|
* Removes all items from the block list
|
|
*/
|
|
public static function unblock_all() {
|
|
self::backend()->unblockAll();
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
* including a head and body tag.
|
|
*
|
|
* @param string $content HTML content that has already been parsed from the $templateFile
|
|
* through {@link SSViewer}
|
|
* @return string HTML content augmented with the requirements tags
|
|
*/
|
|
public static function includeInHTML($content) {
|
|
if(func_num_args() > 1) {
|
|
Deprecation::notice(
|
|
'5.0',
|
|
'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
|
|
);
|
|
$content = func_get_arg(1);
|
|
}
|
|
|
|
return self::backend()->includeInHTML($content);
|
|
}
|
|
|
|
/**
|
|
* Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
|
|
* HTTP Response
|
|
*
|
|
* @param SS_HTTPResponse $response
|
|
*/
|
|
public static function include_in_response(SS_HTTPResponse $response) {
|
|
self::backend()->includeInResponse($response);
|
|
}
|
|
|
|
/**
|
|
* Add i18n files from the given javascript directory. SilverStripe expects that the given
|
|
* directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js,
|
|
* etc.
|
|
*
|
|
* @param string $langDir The JavaScript lang directory, relative to the site root, e.g.,
|
|
* 'framework/javascript/lang'
|
|
* @param bool $return Return all relative file paths rather than including them in
|
|
* requirements
|
|
* @param bool $langOnly Only include language files, not the base libraries
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function add_i18n_javascript($langDir, $return = false, $langOnly = false) {
|
|
return self::backend()->add_i18n_javascript($langDir, $return, $langOnly);
|
|
}
|
|
|
|
/**
|
|
* Concatenate several css or javascript files into a single dynamically generated file. 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.
|
|
*
|
|
* All combined files will have a comment on the start of each concatenated file denoting their
|
|
* original position.
|
|
*
|
|
* 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 incorrect inheritance. You can also
|
|
* only include each file once across all includes and comibinations in a single page load.
|
|
*
|
|
* CAUTION: Combining CSS Files discards any "media" information.
|
|
*
|
|
* Example for combined JavaScript:
|
|
* <code>
|
|
* Requirements::combine_files(
|
|
* 'foobar.js',
|
|
* array(
|
|
* 'mysite/javascript/foo.js',
|
|
* 'mysite/javascript/bar.js',
|
|
* )
|
|
* );
|
|
* </code>
|
|
*
|
|
* Example for combined CSS:
|
|
* <code>
|
|
* Requirements::combine_files(
|
|
* 'foobar.css',
|
|
* array(
|
|
* 'mysite/javascript/foo.css',
|
|
* 'mysite/javascript/bar.css',
|
|
* )
|
|
* );
|
|
* </code>
|
|
*
|
|
* @param string $combinedFileName Filename of the combined file relative to docroot
|
|
* @param array $files Array of filenames relative to docroot
|
|
* @param array $options Array of options for combining files. Available options are:
|
|
* - 'media' : If including CSS Files, you can specify a media type
|
|
* - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
|
|
* - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
|
|
*
|
|
* @return bool|void
|
|
*/
|
|
public static function combine_files($combinedFileName, $files, $options = array()) {
|
|
if(is_string($options)) {
|
|
Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
|
|
$options = array('media' => $options);
|
|
}
|
|
self::backend()->combineFiles($combinedFileName, $files, $options);
|
|
}
|
|
|
|
/**
|
|
* Return all combined files; keys are the combined file names, values are lists of
|
|
* associative arrays with 'files', 'type', and 'media' keys for details about this
|
|
* combined file.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_combine_files() {
|
|
return self::backend()->getCombinedFiles();
|
|
}
|
|
|
|
/**
|
|
* Deletes all generated combined files in the configured combined files directory,
|
|
* but doesn't delete the directory itself
|
|
*/
|
|
public static function delete_all_combined_files() {
|
|
self::backend()->deleteAllCombinedFiles();
|
|
}
|
|
|
|
/**
|
|
* Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
|
|
*/
|
|
public static function clear_combined_files() {
|
|
self::backend()->clearCombinedFiles();
|
|
}
|
|
|
|
/**
|
|
* Do the heavy lifting involved in combining the combined files.
|
|
*/
|
|
public static function process_combined_files() {
|
|
self::backend()->processCombinedFiles();
|
|
}
|
|
|
|
/**
|
|
* Set whether you want to write the JS to the body of the page rather than at the end of the
|
|
* head tag.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_write_js_to_body() {
|
|
return self::backend()->getWriteJavascriptToBody();
|
|
}
|
|
|
|
/**
|
|
* Set whether you want to write the JS to the body of the page rather than at the end of the
|
|
* head tag.
|
|
*
|
|
* @param bool
|
|
*/
|
|
public static function set_write_js_to_body($var) {
|
|
self::backend()->setWriteJavascriptToBody($var);
|
|
}
|
|
|
|
/**
|
|
* Get whether to force the JavaScript to end of the body. Useful if you use inline script tags
|
|
* that don't rely on scripts included via {@link Requirements::javascript()).
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_force_js_to_bottom() {
|
|
return self::backend()->getForceJSToBottom();
|
|
}
|
|
|
|
/**
|
|
* Set whether to force the JavaScript to end of the body. Useful if you use inline script tags
|
|
* that don't rely on scripts included via {@link Requirements::javascript()).
|
|
*
|
|
* @param bool $var If true, force the JavaScript to be included at the bottom of the page
|
|
*/
|
|
public static function set_force_js_to_bottom($var) {
|
|
self::backend()->setForceJSToBottom($var);
|
|
}
|
|
|
|
/**
|
|
* Check if JS minification is enabled
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_minify_combined_js_files() {
|
|
return self::backend()->getMinifyCombinedJSFiles();
|
|
}
|
|
|
|
/**
|
|
* Enable or disable js minification
|
|
*
|
|
* @param bool $minify
|
|
*/
|
|
public static function set_minify_combined_js_files($minify) {
|
|
self::backend()->setMinifyCombinedJSFiles($minify);
|
|
}
|
|
|
|
/**
|
|
* Check if header comments are written
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function get_write_header_comments() {
|
|
return self::backend()->getWriteHeaderComment();
|
|
}
|
|
|
|
/**
|
|
* Flag whether header comments should be written for each combined file
|
|
*
|
|
* @param bool $write
|
|
*/
|
|
public function set_write_header_comments($write) {
|
|
self::backend()->setWriteHeaderComment($write);
|
|
}
|
|
|
|
|
|
/**
|
|
* Output debugging information
|
|
*/
|
|
public static function debug() {
|
|
self::backend()->debug();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* @package framework
|
|
* @subpackage view
|
|
*/
|
|
class Requirements_Backend
|
|
{
|
|
|
|
/**
|
|
* Whether to add caching query params to the requests for file-based requirements.
|
|
* Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
|
|
* filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
|
|
* while automatically busting this cache every time the file is changed.
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $suffixRequirements = true;
|
|
|
|
/**
|
|
* Whether to combine CSS and JavaScript files
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $combinedFilesEnabled = true;
|
|
|
|
/**
|
|
* Determine if files should be combined automatically on dev mode.
|
|
*
|
|
* By default combined files will not be combined except in test or
|
|
* live environments. Turning this on will allow for pre-combining of files in development mode.
|
|
*
|
|
* @config
|
|
* @var bool
|
|
*/
|
|
private static $combine_in_dev = false;
|
|
|
|
/**
|
|
* Paths to all required JavaScript files relative to docroot
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $javascript = array();
|
|
|
|
/**
|
|
* Map of included scripts to array of contained files.
|
|
* To be used alongside front-end combination mechanisms.
|
|
*
|
|
* @var array Map of providing filepath => array(provided filepaths)
|
|
*/
|
|
protected $providedJavascript = array();
|
|
|
|
/**
|
|
* Paths to all required CSS files relative to the docroot.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $css = array();
|
|
|
|
/**
|
|
* All custom javascript code that is inserted into the page's HTML
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $customScript = array();
|
|
|
|
/**
|
|
* All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $customCSS = array();
|
|
|
|
/**
|
|
* All custom HTML markup which is added before the closing <head> tag, e.g. additional
|
|
* metatags.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $customHeadTags = array();
|
|
|
|
/**
|
|
* Remembers the file paths or uniquenessIDs of all Requirements cleared through
|
|
* {@link clear()}, so that they can be restored later.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $disabled = array();
|
|
|
|
/**
|
|
* The file paths (relative to docroot) or uniquenessIDs of any included requirements which
|
|
* should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example,
|
|
* to block scripts included by a superclass without having to override entire functions and
|
|
* duplicate a lot of code.
|
|
*
|
|
* Use {@link unblock()} or {@link unblock_all()} to revert changes.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $blocked = array();
|
|
|
|
/**
|
|
* A list of combined files registered via {@link combine_files()}. Keys are the output file
|
|
* names, values are lists of input files.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $combinedFiles = array();
|
|
|
|
/**
|
|
* Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $minifyCombinedJSFiles = true;
|
|
|
|
/**
|
|
* Whether or not file headers should be written when combining files
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $writeHeaderComment = true;
|
|
|
|
/**
|
|
* Where to save combined files. By default they're placed in assets/_combinedfiles, however
|
|
* this may be an issue depending on your setup, especially for CSS files which often contain
|
|
* relative paths.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $combinedFilesFolder = null;
|
|
|
|
/**
|
|
* Put all JavaScript includes at the bottom of the template before the closing <body> tag,
|
|
* rather than the default behaviour of placing them at the end of the <head> tag. This means
|
|
* script downloads won't block other HTTP requests, which can be a performance improvement.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $writeJavascriptToBody = true;
|
|
|
|
/**
|
|
* Force the JavaScript to the bottom of the page, even if there's a script tag in the body already
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $forceJSToBottom = false;
|
|
|
|
/**
|
|
* Configures the default prefix for combined files.
|
|
*
|
|
* This defaults to `_combinedfiles`, and is the folder within the configured asset backend that
|
|
* combined files will be stored in. If using a backend shared with other systems, it is usually
|
|
* necessary to distinguish combined files from other assets.
|
|
*
|
|
* @config
|
|
* @var string
|
|
*/
|
|
private static $default_combined_files_folder = '_combinedfiles';
|
|
|
|
/**
|
|
* Flag to include the hash in the querystring instead of the filename for combined files.
|
|
*
|
|
* By default the `<hash>` of the source files is appended to the end of the combined file
|
|
* (prior to the file extension). If combined files are versioned in source control or running
|
|
* in a distributed environment (such as one where the newest version of a file may not always be
|
|
* immediately available) then it may sometimes be necessary to disable this. When this is set to true,
|
|
* the hash will instead be appended via a querystring parameter to enable cache busting, but not in
|
|
* the filename itself. I.e. `assets/_combinedfiles/name.js?m=<hash>`
|
|
*
|
|
* @config
|
|
* @var bool
|
|
*/
|
|
private static $combine_hash_querystring = false;
|
|
|
|
/**
|
|
* @var GeneratedAssetHandler
|
|
*/
|
|
protected $assetHandler = null;
|
|
|
|
/**
|
|
* Gets the backend storage for generated files
|
|
*
|
|
* @return GeneratedAssetHandler
|
|
*/
|
|
public function getAssetHandler() {
|
|
return $this->assetHandler;
|
|
}
|
|
|
|
/**
|
|
* Set a new asset handler for this backend
|
|
*
|
|
* @param GeneratedAssetHandler $handler
|
|
*/
|
|
public function setAssetHandler(GeneratedAssetHandler $handler) {
|
|
$this->assetHandler = $handler;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable the combination of CSS and JavaScript files
|
|
*
|
|
* @param bool $enable
|
|
*/
|
|
public function setCombinedFilesEnabled($enable) {
|
|
$this->combinedFilesEnabled = (bool) $enable;
|
|
}
|
|
|
|
/**
|
|
* Check if header comments are written
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getWriteHeaderComment() {
|
|
return $this->writeHeaderComment;
|
|
}
|
|
|
|
/**
|
|
* Flag whether header comments should be written for each combined file
|
|
*
|
|
* @param bool $write
|
|
* @return $this
|
|
*/
|
|
public function setWriteHeaderComment($write) {
|
|
$this->writeHeaderComment = $write;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the folder to save combined files in. By default they're placed in _combinedfiles,
|
|
* however this may be an issue depending on your setup, especially for CSS files which often
|
|
* contain relative paths.
|
|
*
|
|
* This must not include any 'assets' prefix
|
|
*
|
|
* @param string $folder
|
|
*/
|
|
public function setCombinedFilesFolder($folder) {
|
|
$this->combinedFilesFolder = $folder;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the combined files folder prefix
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCombinedFilesFolder() {
|
|
if($this->combinedFilesFolder) {
|
|
return $this->combinedFilesFolder;
|
|
}
|
|
return Config::inst()->get(__CLASS__, 'default_combined_files_folder');
|
|
}
|
|
|
|
/**
|
|
* Set whether to add caching query params to the requests for file-based requirements.
|
|
* Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by
|
|
* filemtime. This has the benefit of allowing the browser to cache the URL infinitely,
|
|
* while automatically busting this cache every time the file is changed.
|
|
*
|
|
* @param bool
|
|
*/
|
|
public function setSuffixRequirements($var) {
|
|
$this->suffixRequirements = $var;
|
|
}
|
|
|
|
/**
|
|
* Check whether we want to suffix requirements
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getSuffixRequirements() {
|
|
return $this->suffixRequirements;
|
|
}
|
|
|
|
/**
|
|
* Set whether you want to write the JS to the body of the page rather than at the end of the
|
|
* head tag.
|
|
*
|
|
* @param bool
|
|
* @return $this
|
|
*/
|
|
public function setWriteJavascriptToBody($var) {
|
|
$this->writeJavascriptToBody = $var;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check whether you want to write the JS to the body of the page rather than at the end of the
|
|
* head tag.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getWriteJavascriptToBody() {
|
|
return $this->writeJavascriptToBody;
|
|
}
|
|
|
|
/**
|
|
* Forces the JavaScript requirements to the end of the body, right before the closing tag
|
|
*
|
|
* @param bool
|
|
* @return $this
|
|
*/
|
|
public function setForceJSToBottom($var) {
|
|
$this->forceJSToBottom = $var;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Check if the JavaScript requirements are written to the end of the body, right before the closing tag
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getForceJSToBottom() {
|
|
return $this->forceJSToBottom;
|
|
}
|
|
|
|
/**
|
|
* Check if minify js files should be combined
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getMinifyCombinedJSFiles() {
|
|
return $this->minifyCombinedJSFiles;
|
|
}
|
|
|
|
/**
|
|
* Set if combined js files should be minified
|
|
*
|
|
* @param bool $minify
|
|
* @return $this
|
|
*/
|
|
public function setMinifyCombinedJSFiles($minify) {
|
|
$this->minifyCombinedJSFiles = $minify;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Register the given JavaScript file as required.
|
|
*
|
|
* @param string $file Relative to docroot
|
|
* @param array $options List of options. Available options include:
|
|
* - 'provides' : List of scripts files included in this file
|
|
* - 'async' : Boolean value to set async attribute to script tag
|
|
* - 'defer' : Boolean value to set defer attribute to script tag
|
|
* - 'type' : Override script type= value.
|
|
*/
|
|
public function javascript($file, $options = array()) {
|
|
// Get type
|
|
$type = null;
|
|
if (isset($this->javascript[$file]['type'])) {
|
|
$type = $this->javascript[$file]['type'];
|
|
}
|
|
if (isset($options['type'])) {
|
|
$type = $options['type'];
|
|
}
|
|
|
|
// make sure that async/defer is set if it is set once even if file is included multiple times
|
|
$async = (
|
|
isset($options['async']) && isset($options['async']) == true
|
|
|| (
|
|
isset($this->javascript[$file])
|
|
&& isset($this->javascript[$file]['async'])
|
|
&& $this->javascript[$file]['async'] == true
|
|
)
|
|
);
|
|
$defer = (
|
|
isset($options['defer']) && isset($options['defer']) == true
|
|
|| (
|
|
isset($this->javascript[$file])
|
|
&& isset($this->javascript[$file]['defer'])
|
|
&& $this->javascript[$file]['defer'] == true
|
|
)
|
|
);
|
|
$this->javascript[$file] = array(
|
|
'async' => $async,
|
|
'defer' => $defer,
|
|
'type' => $type,
|
|
);
|
|
|
|
// Record scripts included in this file
|
|
if(isset($options['provides'])) {
|
|
$this->providedJavascript[$file] = array_values($options['provides']);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Remove a javascript requirement
|
|
*
|
|
* @param string $file
|
|
*/
|
|
protected function unsetJavascript($file) {
|
|
unset($this->javascript[$file]);
|
|
}
|
|
|
|
/**
|
|
* Gets all scripts that are already provided by prior scripts.
|
|
* This follows these rules:
|
|
* - Files will not be considered provided if they are separately
|
|
* included prior to the providing file.
|
|
* - Providing files can be blocked, and don't provide anything
|
|
* - Provided files can't be blocked (you need to block the provider)
|
|
* - If a combined file includes files that are provided by prior
|
|
* scripts, then these should be excluded from the combined file.
|
|
* - If a combined file includes files that are provided by later
|
|
* scripts, then these files should be included in the combined
|
|
* file, but we can't block the later script either (possible double
|
|
* up of file).
|
|
*
|
|
* @return array Array of provided files (map of $path => $path)
|
|
*/
|
|
public function getProvidedScripts() {
|
|
$providedScripts = array();
|
|
$includedScripts = array();
|
|
foreach($this->javascript as $script => $options) {
|
|
// Ignore scripts that are explicitly blocked
|
|
if(isset($this->blocked[$script])) {
|
|
continue;
|
|
}
|
|
// At this point, the file is included.
|
|
// This might also be combined at this point, potentially.
|
|
$includedScripts[$script] = true;
|
|
|
|
// Record any files this provides, EXCEPT those already included by now
|
|
if(isset($this->providedJavascript[$script])) {
|
|
foreach($this->providedJavascript[$script] as $provided) {
|
|
if(!isset($includedScripts[$provided])) {
|
|
$providedScripts[$provided] = $provided;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $providedScripts;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of required JavaScript, excluding blocked
|
|
* and duplicates of provided files.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getJavascript() {
|
|
return array_diff_key(
|
|
$this->javascript,
|
|
$this->getBlocked(),
|
|
$this->getProvidedScripts()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gets all javascript, including blocked files. Unwraps the array into a non-associative list
|
|
*
|
|
* @return array Indexed array of javascript files
|
|
*/
|
|
protected function getAllJavascript() {
|
|
return $this->javascript;
|
|
}
|
|
|
|
/**
|
|
* Register the given JavaScript code into the list of requirements
|
|
*
|
|
* @param string $script The script content as a string (without enclosing <script> tag)
|
|
* @param string $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public function customScript($script, $uniquenessID = null) {
|
|
if($uniquenessID) {
|
|
$this->customScript[$uniquenessID] = $script;
|
|
} else {
|
|
$this->customScript[] = $script;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return all registered custom scripts
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCustomScripts() {
|
|
return array_diff_key($this->customScript, $this->blocked);
|
|
}
|
|
|
|
/**
|
|
* Register the given CSS styles into the list of requirements
|
|
*
|
|
* @param string $script CSS selectors as a string (without enclosing <style> tag)
|
|
* @param string $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public function customCSS($script, $uniquenessID = null) {
|
|
if($uniquenessID) {
|
|
$this->customCSS[$uniquenessID] = $script;
|
|
} else {
|
|
$this->customCSS[] = $script;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return all registered custom CSS
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCustomCSS() {
|
|
return array_diff_key($this->customCSS, $this->blocked);
|
|
}
|
|
|
|
/**
|
|
* Add the following custom HTML code to the <head> section of the page
|
|
*
|
|
* @param string $html Custom HTML code
|
|
* @param string $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public function insertHeadTags($html, $uniquenessID = null) {
|
|
if($uniquenessID) {
|
|
$this->customHeadTags[$uniquenessID] = $html;
|
|
} else {
|
|
$this->customHeadTags[] = $html;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return all custom head tags
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCustomHeadTags() {
|
|
return array_diff_key($this->customHeadTags, $this->blocked);
|
|
}
|
|
|
|
/**
|
|
* Include the content of the given JavaScript file in the list of requirements. Dollar-sign
|
|
* variables will be interpolated with values from $vars similar to a .ss template.
|
|
*
|
|
* @param string $file The template file to load, relative to docroot
|
|
* @param string[] $vars The array of variables to interpolate.
|
|
* @param string $uniquenessID A unique ID that ensures a piece of code is only added once
|
|
*/
|
|
public function javascriptTemplate($file, $vars, $uniquenessID = null) {
|
|
$script = file_get_contents(Director::getAbsFile($file));
|
|
$search = array();
|
|
$replace = array();
|
|
|
|
if($vars) foreach($vars as $k => $v) {
|
|
$search[] = '$' . $k;
|
|
$replace[] = str_replace("\\'","'", Convert::raw2js($v));
|
|
}
|
|
|
|
$script = str_replace($search, $replace, $script);
|
|
$this->customScript($script, $uniquenessID);
|
|
}
|
|
|
|
/**
|
|
* Register the given stylesheet into the list of requirements.
|
|
*
|
|
* @param string $file The CSS file to load, relative to site root
|
|
* @param string $media Comma-separated list of media types to use in the link tag
|
|
* (e.g. 'screen,projector')
|
|
*/
|
|
public function css($file, $media = null) {
|
|
$this->css[$file] = array(
|
|
"media" => $media
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove a css requirement
|
|
*
|
|
* @param string $file
|
|
*/
|
|
protected function unsetCSS($file) {
|
|
unset($this->css[$file]);
|
|
}
|
|
|
|
/**
|
|
* Get the list of registered CSS file requirements, excluding blocked files
|
|
*
|
|
* @return array Associative array of file to spec
|
|
*/
|
|
public function getCSS() {
|
|
return array_diff_key($this->css, $this->blocked);
|
|
}
|
|
|
|
/**
|
|
* Gets all CSS files requirements, including blocked
|
|
*
|
|
* @return array Associative array of file to spec
|
|
*/
|
|
protected function getAllCSS() {
|
|
return $this->css;
|
|
}
|
|
|
|
/**
|
|
* Gets the list of all blocked files
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getBlocked() {
|
|
return $this->blocked;
|
|
}
|
|
|
|
/**
|
|
* Clear either a single or all requirements
|
|
*
|
|
* Caution: Clearing single rules added via customCSS and customScript only works if you
|
|
* originally specified a $uniquenessID.
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public function clear($fileOrID = null) {
|
|
if($fileOrID) {
|
|
foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) {
|
|
if(isset($this->{$type}[$fileOrID])) {
|
|
$this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID];
|
|
unset($this->{$type}[$fileOrID]);
|
|
}
|
|
}
|
|
} else {
|
|
$this->disabled['javascript'] = $this->javascript;
|
|
$this->disabled['css'] = $this->css;
|
|
$this->disabled['customScript'] = $this->customScript;
|
|
$this->disabled['customCSS'] = $this->customCSS;
|
|
$this->disabled['customHeadTags'] = $this->customHeadTags;
|
|
|
|
$this->javascript = array();
|
|
$this->css = array();
|
|
$this->customScript = array();
|
|
$this->customCSS = array();
|
|
$this->customHeadTags = array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore requirements cleared by call to Requirements::clear
|
|
*/
|
|
public function restore() {
|
|
$this->javascript = $this->disabled['javascript'];
|
|
$this->css = $this->disabled['css'];
|
|
$this->customScript = $this->disabled['customScript'];
|
|
$this->customCSS = $this->disabled['customCSS'];
|
|
$this->customHeadTags = $this->disabled['customHeadTags'];
|
|
}
|
|
|
|
/**
|
|
* Block inclusion of a specific file
|
|
*
|
|
* The difference between this and {@link clear} is that the calling order does not matter;
|
|
* {@link clear} must be called after the initial registration, whereas {@link block} can be
|
|
* used in advance. This is useful, for example, to block scripts included by a superclass
|
|
* without having to override entire functions and duplicate a lot of code.
|
|
*
|
|
* Note that blocking should be used sparingly because it's hard to trace where an file is
|
|
* being blocked from.
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public function block($fileOrID) {
|
|
$this->blocked[$fileOrID] = $fileOrID;
|
|
}
|
|
|
|
/**
|
|
* Remove an item from the block list
|
|
*
|
|
* @param string|int $fileOrID
|
|
*/
|
|
public function unblock($fileOrID) {
|
|
unset($this->blocked[$fileOrID]);
|
|
}
|
|
|
|
/**
|
|
* Removes all items from the block list
|
|
*/
|
|
public function unblockAll() {
|
|
$this->blocked = array();
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
* including a head and body tag.
|
|
*
|
|
* @param string $content HTML content that has already been parsed from the $templateFile
|
|
* through {@link SSViewer}
|
|
* @return string HTML content augmented with the requirements tags
|
|
*/
|
|
public function includeInHTML($content) {
|
|
if(func_num_args() > 1) {
|
|
Deprecation::notice(
|
|
'5.0',
|
|
'$templateFile argument is deprecated. includeInHTML takes a sole $content parameter now.'
|
|
);
|
|
$content = func_get_arg(1);
|
|
}
|
|
|
|
// Skip if content isn't injectable, or there is nothing to inject
|
|
$tagsAvailable = preg_match('#</head\b#', $content);
|
|
$hasFiles = $this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags;
|
|
if(!$tagsAvailable || !$hasFiles) {
|
|
return $content;
|
|
}
|
|
$requirements = '';
|
|
$jsRequirements = '';
|
|
|
|
// Combine files - updates $this->javascript and $this->css
|
|
$this->processCombinedFiles();
|
|
|
|
foreach($this->getJavascript() as $file => $attributes) {
|
|
$async = (isset($attributes['async']) && $attributes['async'] == true) ? " async" : "";
|
|
$defer = (isset($attributes['defer']) && $attributes['defer'] == true) ? " defer" : "";
|
|
$type = Convert::raw2att(isset($attributes['type']) ? $attributes['type'] : "application/javascript");
|
|
$path = Convert::raw2att($this->pathForFile($file));
|
|
if($path) {
|
|
$jsRequirements .= "<script type=\"{$type}\" src=\"{$path}\"{$async}{$defer}></script>";
|
|
}
|
|
}
|
|
|
|
// Add all inline JavaScript *after* including external files they might rely on
|
|
foreach($this->getCustomScripts() as $script) {
|
|
$jsRequirements .= "<script type=\"application/javascript\">//<![CDATA[\n";
|
|
$jsRequirements .= "$script\n";
|
|
$jsRequirements .= "//]]></script>";
|
|
}
|
|
|
|
foreach($this->getCSS() as $file => $params) {
|
|
$path = Convert::raw2att($this->pathForFile($file));
|
|
if($path) {
|
|
$media = (isset($params['media']) && !empty($params['media']))
|
|
? " media=\"{$params['media']}\"" : "";
|
|
$requirements .= "<link rel=\"stylesheet\" type=\"text/css\" {$media} href=\"$path\" />\n";
|
|
}
|
|
}
|
|
|
|
foreach($this->getCustomCSS() as $css) {
|
|
$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
|
|
}
|
|
|
|
foreach($this->getCustomHeadTags() as $customHeadTag) {
|
|
$requirements .= "$customHeadTag\n";
|
|
}
|
|
|
|
// Inject CSS into body
|
|
$content = $this->insertTagsIntoHead($requirements, $content);
|
|
|
|
// Inject scripts
|
|
if ($this->getForceJSToBottom()) {
|
|
$content = $this->insertScriptsAtBottom($jsRequirements, $content);
|
|
} elseif($this->getWriteJavascriptToBody()) {
|
|
$content = $this->insertScriptsIntoBody($jsRequirements, $content);
|
|
} else {
|
|
$content = $this->insertTagsIntoHead($jsRequirements, $content);
|
|
}
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Given a block of HTML, insert the given scripts at the bottom before
|
|
* the closing </body> tag
|
|
*
|
|
* @param string $jsRequirements String containing one or more javascript <script /> tags
|
|
* @param string $content HTML body
|
|
* @return string Merged HTML
|
|
*/
|
|
protected function insertScriptsAtBottom($jsRequirements, $content) {
|
|
// Forcefully put the scripts at the bottom of the body instead of before the first
|
|
// script tag.
|
|
$content = preg_replace(
|
|
'/(<\/body[^>]*>)/i',
|
|
$this->escapeReplacement($jsRequirements) . '\\1',
|
|
$content
|
|
);
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Given a block of HTML, insert the given scripts inside the <body></body>
|
|
*
|
|
* @param string $jsRequirements String containing one or more javascript <script /> tags
|
|
* @param string $content HTML body
|
|
* @return string Merged HTML
|
|
*/
|
|
protected function insertScriptsIntoBody($jsRequirements, $content) {
|
|
// 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.
|
|
$bodyTagPosition = stripos($content, '<body');
|
|
$scriptTagPosition = stripos($content, '<script', $bodyTagPosition);
|
|
|
|
$commentTags = array();
|
|
$canWriteToBody = ($scriptTagPosition !== false)
|
|
&&
|
|
// Check that the script tag is not inside a html comment tag
|
|
!(
|
|
preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $scriptTagPosition)
|
|
&&
|
|
$commentTags[1] == '-->'
|
|
);
|
|
|
|
if($canWriteToBody) {
|
|
// Insert content before existing script tags
|
|
$content = substr($content, 0, $scriptTagPosition)
|
|
. $jsRequirements
|
|
. substr($content, $scriptTagPosition);
|
|
} else {
|
|
// Insert content at bottom of page otherwise
|
|
$content = $this->insertScriptsAtBottom($jsRequirements, $content);
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Given a block of HTML, insert the given code inside the <head></head> block
|
|
*
|
|
* @param string $jsRequirements String containing one or more html tags
|
|
* @param string $content HTML body
|
|
* @return string Merged HTML
|
|
*/
|
|
protected function insertTagsIntoHead($jsRequirements, $content) {
|
|
$content = preg_replace(
|
|
'/(<\/head>)/i',
|
|
$this->escapeReplacement($jsRequirements) . '\\1',
|
|
$content
|
|
);
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Safely escape a literal string for use in preg_replace replacement
|
|
*
|
|
* @param string $replacement
|
|
* @return string
|
|
*/
|
|
protected function escapeReplacement($replacement) {
|
|
return addcslashes($replacement, '\\$');
|
|
}
|
|
|
|
/**
|
|
* Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given
|
|
* HTTP Response
|
|
*
|
|
* @param SS_HTTPResponse $response
|
|
*/
|
|
public function includeInResponse(SS_HTTPResponse $response) {
|
|
$this->processCombinedFiles();
|
|
$jsRequirements = array();
|
|
$cssRequirements = array();
|
|
|
|
foreach($this->getJavascript() as $file => $attributes) {
|
|
$path = $this->pathForFile($file);
|
|
if($path) {
|
|
$jsRequirements[] = str_replace(',', '%2C', $path);
|
|
}
|
|
}
|
|
|
|
if(count($jsRequirements)) {
|
|
$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
|
|
}
|
|
|
|
foreach($this->getCSS() as $file => $params) {
|
|
$path = $this->pathForFile($file);
|
|
if($path) {
|
|
$path = str_replace(',', '%2C', $path);
|
|
$cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path;
|
|
}
|
|
}
|
|
|
|
if(count($cssRequirements)) {
|
|
$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 JavaScript files named by language: en_US.js, de_DE.js,
|
|
* etc.
|
|
*
|
|
* @param string $langDir The JavaScript lang directory, relative to the site root, e.g.,
|
|
* 'framework/javascript/lang'
|
|
* @param bool $return Return all relative file paths rather than including them in
|
|
* requirements
|
|
* @param bool $langOnly Only include language files, not the base libraries
|
|
*
|
|
* @return array|null All relative files if $return is true, or null otherwise
|
|
*/
|
|
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 . '/client/dist/js/i18n.js';
|
|
|
|
if(substr($langDir,-1) != '/') $langDir .= '/';
|
|
|
|
$candidates = array(
|
|
'en.js',
|
|
'en_US.js',
|
|
i18n::get_lang_from_locale(i18n::config()->default_locale) . '.js',
|
|
i18n::config()->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 . '/client/dist/js/i18nx.js';
|
|
}
|
|
}
|
|
|
|
if($return) {
|
|
return $files;
|
|
} else {
|
|
foreach($files as $file) {
|
|
$this->javascript($file);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the path for specified file
|
|
*
|
|
* @param string $fileOrUrl
|
|
* @return string|bool
|
|
*/
|
|
protected function pathForFile($fileOrUrl) {
|
|
// Since combined urls could be root relative, treat them as urls here.
|
|
if(preg_match('{^(//)|(http[s]?:)}', $fileOrUrl) || Director::is_root_relative_url($fileOrUrl)) {
|
|
return $fileOrUrl;
|
|
} elseif(Director::fileExists($fileOrUrl)) {
|
|
$filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl);
|
|
$prefix = Director::baseURL();
|
|
$mtimesuffix = "";
|
|
$suffix = '';
|
|
if($this->getSuffixRequirements()) {
|
|
$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 {
|
|
throw new InvalidArgumentException("File {$fileOrUrl} does not exist");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Concatenate several css or javascript files into a single dynamically generated file. 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.
|
|
*
|
|
* All combined files will have a comment on the start of each concatenated file denoting their
|
|
* original position.
|
|
*
|
|
* 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 incorrect inheritance. You can also
|
|
* only include each file once across all includes and combinations in a single page load.
|
|
*
|
|
* CAUTION: Combining CSS Files discards any "media" information.
|
|
*
|
|
* Example for combined JavaScript:
|
|
* <code>
|
|
* Requirements::combine_files(
|
|
* 'foobar.js',
|
|
* array(
|
|
* 'mysite/javascript/foo.js',
|
|
* 'mysite/javascript/bar.js',
|
|
* ),
|
|
* array(
|
|
* 'async' => true,
|
|
* 'defer' => true,
|
|
* )
|
|
* );
|
|
* </code>
|
|
*
|
|
* Example for combined CSS:
|
|
* <code>
|
|
* Requirements::combine_files(
|
|
* 'foobar.css',
|
|
* array(
|
|
* 'mysite/javascript/foo.css',
|
|
* 'mysite/javascript/bar.css',
|
|
* ),
|
|
* array(
|
|
* 'media' => 'print',
|
|
* )
|
|
* );
|
|
* </code>
|
|
*
|
|
* @param string $combinedFileName Filename of the combined file relative to docroot
|
|
* @param array $files Array of filenames relative to docroot
|
|
* @param array $options Array of options for combining files. Available options are:
|
|
* - 'media' : If including CSS Files, you can specify a media type
|
|
* - 'async' : If including JavaScript Files, boolean value to set async attribute to script tag
|
|
* - 'defer' : If including JavaScript Files, boolean value to set defer attribute to script tag
|
|
*/
|
|
public function combineFiles($combinedFileName, $files, $options = array()) {
|
|
if(is_string($options)) {
|
|
Deprecation::notice('4.0', 'Parameter media is deprecated. Use options array instead.');
|
|
$options = array('media' => $options);
|
|
}
|
|
// Skip this combined files if already included
|
|
if(isset($this->combinedFiles[$combinedFileName])) {
|
|
return;
|
|
}
|
|
|
|
// Add all files to necessary type list
|
|
$paths = array();
|
|
$combinedType = null;
|
|
foreach($files as $file) {
|
|
// Get file details
|
|
list($path, $type) = $this->parseCombinedFile($file);
|
|
if($type === 'javascript') {
|
|
$type = 'js';
|
|
}
|
|
if($combinedType && $type && $combinedType !== $type) {
|
|
throw new InvalidArgumentException(
|
|
"Cannot mix js and css files in same combined file {$combinedFileName}"
|
|
);
|
|
}
|
|
switch($type) {
|
|
case 'css':
|
|
$this->css($path, (isset($options['media']) ? $options['media'] : null));
|
|
break;
|
|
case 'js':
|
|
$this->javascript($path, $options);
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException("Invalid combined file type: {$type}");
|
|
}
|
|
$combinedType = $type;
|
|
$paths[] = $path;
|
|
}
|
|
|
|
// Duplicate check
|
|
foreach($this->combinedFiles as $existingCombinedFilename => $combinedItem) {
|
|
$existingFiles = $combinedItem['files'];
|
|
$duplicates = array_intersect($existingFiles, $paths);
|
|
if($duplicates) {
|
|
throw new InvalidArgumentException(sprintf(
|
|
"Requirements_Backend::combine_files(): Already included file(s) %s in combined file '%s'",
|
|
implode(',', $duplicates),
|
|
$existingCombinedFilename
|
|
));
|
|
}
|
|
}
|
|
|
|
$this->combinedFiles[$combinedFileName] = array(
|
|
'files' => $paths,
|
|
'type' => $combinedType,
|
|
'options' => $options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return path and type of given combined file
|
|
*
|
|
* @param string|array $file Either a file path, or an array spec
|
|
* @return array array with two elements, path and type of file
|
|
*/
|
|
protected function parseCombinedFile($file) {
|
|
// Array with path and type keys
|
|
if(is_array($file) && isset($file['path']) && isset($file['type'])) {
|
|
return array($file['path'], $file['type']);
|
|
}
|
|
|
|
// Extract value from indexed array
|
|
if(is_array($file)) {
|
|
$path = array_shift($file);
|
|
|
|
// See if there's a type specifier
|
|
if($file) {
|
|
$type = array_shift($file);
|
|
return array($path, $type);
|
|
}
|
|
|
|
// Otherwise convent to string
|
|
$file = $path;
|
|
}
|
|
|
|
$type = File::get_file_extension($file);
|
|
return array($file, $type);
|
|
}
|
|
|
|
/**
|
|
* Return all combined files; keys are the combined file names, values are lists of
|
|
* associative arrays with 'files', 'type', and 'media' keys for details about this
|
|
* combined file.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCombinedFiles() {
|
|
return array_diff_key($this->combinedFiles, $this->blocked);
|
|
}
|
|
|
|
/**
|
|
* Includes all combined files, including blocked ones
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getAllCombinedFiles() {
|
|
return $this->combinedFiles;
|
|
}
|
|
|
|
/**
|
|
* Clears all combined files
|
|
*/
|
|
public function deleteAllCombinedFiles() {
|
|
$combinedFolder = $this->getCombinedFilesFolder();
|
|
if($combinedFolder) {
|
|
$this->getAssetHandler()->removeContent($combinedFolder);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all registered CSS and JavaScript file combinations
|
|
*/
|
|
public function clearCombinedFiles() {
|
|
$this->combinedFiles = array();
|
|
}
|
|
|
|
/**
|
|
* Do the heavy lifting involved in combining the combined files.
|
|
*/
|
|
public function processCombinedFiles() {
|
|
// Check if combining is enabled
|
|
if(!$this->getCombinedFilesEnabled()) {
|
|
return;
|
|
}
|
|
|
|
// Before scripts are modified, detect files that are provided by preceding ones
|
|
$providedScripts = $this->getProvidedScripts();
|
|
|
|
// Process each combined files
|
|
foreach($this->getAllCombinedFiles() as $combinedFile => $combinedItem) {
|
|
$fileList = $combinedItem['files'];
|
|
$type = $combinedItem['type'];
|
|
$options = $combinedItem['options'];
|
|
|
|
// Generate this file, unless blocked
|
|
$combinedURL = null;
|
|
if(!isset($this->blocked[$combinedFile])) {
|
|
// Filter files for blocked / provided
|
|
$filteredFileList = array_diff(
|
|
$fileList,
|
|
$this->getBlocked(),
|
|
$providedScripts
|
|
);
|
|
$combinedURL = $this->getCombinedFileURL($combinedFile, $filteredFileList, $type);
|
|
}
|
|
|
|
// Replace all existing files, injecting the combined file at the position of the first item
|
|
// in order to preserve inclusion order.
|
|
// Note that we iterate across blocked files in order to get the correct order, and validate
|
|
// that the file is included in the correct location (regardless of which files are blocked).
|
|
$included = false;
|
|
switch($type) {
|
|
case 'css': {
|
|
$newCSS = array(); // Assoc array of css file => spec
|
|
foreach($this->getAllCSS() as $css => $spec) {
|
|
if(!in_array($css, $fileList)) {
|
|
$newCSS[$css] = $spec;
|
|
} elseif(!$included && $combinedURL) {
|
|
$newCSS[$combinedURL] = array('media' => (isset($options['media']) ? $options['media'] : null));
|
|
$included = true;
|
|
}
|
|
// If already included, or otherwise blocked, then don't add into CSS
|
|
}
|
|
$this->css = $newCSS;
|
|
break;
|
|
}
|
|
case 'js': {
|
|
// Assoc array of file => attributes
|
|
$newJS = array();
|
|
foreach($this->getAllJavascript() as $script => $attributes) {
|
|
if(!in_array($script, $fileList)) {
|
|
$newJS[$script] = $attributes;
|
|
} elseif(!$included && $combinedURL) {
|
|
$newJS[$combinedURL] = $options;
|
|
$included = true;
|
|
}
|
|
// If already included, or otherwise blocked, then don't add into scripts
|
|
}
|
|
$this->javascript = $newJS;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a set of files, combine them (as necessary) and return the url
|
|
*
|
|
* @param string $combinedFile Filename for this combined file
|
|
* @param array $fileList List of files to combine
|
|
* @param string $type Either 'js' or 'css'
|
|
* @return string|null URL to this resource, if there are files to combine
|
|
*/
|
|
protected function getCombinedFileURL($combinedFile, $fileList, $type) {
|
|
// Skip empty lists
|
|
if(empty($fileList)) {
|
|
return null;
|
|
}
|
|
|
|
// Generate path (Filename)
|
|
$hashQuerystring = Config::inst()->get(static::class, 'combine_hash_querystring');
|
|
if(!$hashQuerystring) {
|
|
$combinedFile = $this->hashedCombinedFilename($combinedFile, $fileList);
|
|
}
|
|
$combinedFileID = File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
|
|
|
|
// Send file combination request to the backend, with an optional callback to perform regeneration
|
|
$minify = $this->getMinifyCombinedJSFiles();
|
|
$combinedURL = $this
|
|
->getAssetHandler()
|
|
->getContentURL(
|
|
$combinedFileID,
|
|
function() use ($fileList, $minify, $type) {
|
|
// Physically combine all file content
|
|
$combinedData = '';
|
|
$base = Director::baseFolder() . '/';
|
|
$minifier = Injector::inst()->get('Requirements_Minifier');
|
|
foreach($fileList as $file) {
|
|
$fileContent = file_get_contents($base . $file);
|
|
// Use configured minifier
|
|
if($minify) {
|
|
$fileContent = $minifier->minify($fileContent, $type, $file);
|
|
}
|
|
|
|
if ($this->writeHeaderComment) {
|
|
// Write a header comment for each file for easier identification and debugging.
|
|
$combinedData .= "/****** FILE: $file *****/\n";
|
|
}
|
|
$combinedData .= $fileContent . "\n";
|
|
}
|
|
return $combinedData;
|
|
}
|
|
);
|
|
|
|
// If the name isn't hashed, we will need to append the querystring m= parameter instead
|
|
// Since url won't be automatically suffixed, add it in here
|
|
if($hashQuerystring && $this->getSuffixRequirements()) {
|
|
$hash = $this->hashOfFiles($fileList);
|
|
$q = stripos($combinedURL, '?') === false ? '?' : '&';
|
|
$combinedURL .= "{$q}m={$hash}";
|
|
}
|
|
|
|
return $combinedURL;
|
|
}
|
|
|
|
/**
|
|
* Given a filename and list of files, generate a new filename unique to these files
|
|
*
|
|
* @param string $combinedFile
|
|
* @param array $fileList
|
|
* @return string
|
|
*/
|
|
protected function hashedCombinedFilename($combinedFile, $fileList) {
|
|
$name = pathinfo($combinedFile, PATHINFO_FILENAME);
|
|
$hash = $this->hashOfFiles($fileList);
|
|
$extension = File::get_file_extension($combinedFile);
|
|
return $name . '-' . substr($hash, 0, 7) . '.' . $extension;
|
|
}
|
|
|
|
/**
|
|
* Check if combined files are enabled
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getCombinedFilesEnabled() {
|
|
if(!$this->combinedFilesEnabled) {
|
|
return false;
|
|
}
|
|
|
|
// Tests should be combined
|
|
if(class_exists('SapphireTest', false) && SapphireTest::is_running_test()) {
|
|
return true;
|
|
}
|
|
|
|
// Check if specified via querystring
|
|
if(isset($_REQUEST['combine'])) {
|
|
return true;
|
|
}
|
|
|
|
// Non-dev sites are always combined
|
|
if(!Director::isDev()) {
|
|
return true;
|
|
}
|
|
|
|
// Fallback to default
|
|
return Config::inst()->get(__CLASS__, 'combine_in_dev');
|
|
}
|
|
|
|
/**
|
|
* For a given filelist, determine some discriminating value to determine if
|
|
* any of these files have changed.
|
|
*
|
|
* @param array $fileList List of files
|
|
* @return string SHA1 bashed file hash
|
|
*/
|
|
protected function hashOfFiles($fileList) {
|
|
// Get hash based on hash of each file
|
|
$base = Director::baseFolder() . '/';
|
|
$hash = '';
|
|
foreach($fileList as $file) {
|
|
if(file_exists($base . $file)) {
|
|
$hash .= sha1_file($base . $file);
|
|
} else {
|
|
throw new InvalidArgumentException("Combined file {$file} does not exist");
|
|
}
|
|
}
|
|
return sha1($hash);
|
|
}
|
|
|
|
/**
|
|
* Registers the given themeable stylesheet as required.
|
|
*
|
|
* A CSS file in the current theme path name 'themename/css/$name.css' is first searched for,
|
|
* and it that doesn't exist and the module parameter is set then a CSS file with that name in
|
|
* the module is used.
|
|
*
|
|
* @param string $name The name of the file - eg '/css/File.css' would have the name 'File'
|
|
* @param string $media Comma-separated list of media types to use in the link tag
|
|
* (e.g. 'screen,projector')
|
|
*/
|
|
public function themedCSS($name, $media = null) {
|
|
$path = ThemeResourceLoader::instance()->findThemedCSS($name, SSViewer::get_themes());
|
|
if($path) {
|
|
$this->css($path, $media);
|
|
} else {
|
|
throw new \InvalidArgumentException(
|
|
"The css file doesn't exists. Please check if the file $name.css exists in any context or search for "
|
|
. "themedCSS references calling this file in your templates."
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers the given themeable javascript as required.
|
|
*
|
|
* A javascript file in the current theme path name 'themename/javascript/$name.js' is first searched for,
|
|
* and it that doesn't exist and the module parameter is set then a javascript file with that name in
|
|
* the module is used.
|
|
*
|
|
* @param string $name The name of the file - eg '/js/File.js' would have the name 'File'
|
|
* @param string $type Comma-separated list of types to use in the script tag
|
|
* (e.g. 'text/javascript,text/ecmascript')
|
|
*/
|
|
public function themedJavascript($name, $type = null) {
|
|
$path = ThemeResourceLoader::instance()->findThemedJavascript($name, SSViewer::get_themes());
|
|
if($path) {
|
|
$opts = [];
|
|
if($type) {
|
|
$opts['type'] = $type;
|
|
}
|
|
$this->javascript($path, $opts);
|
|
} else {
|
|
throw new \InvalidArgumentException(
|
|
"The javascript file doesn't exists. Please check if the file $name.js exists in any "
|
|
. "context or search for themedJavascript references calling this file in your templates."
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Output debugging information.
|
|
*/
|
|
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->combinedFiles);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Provides an abstract interface for minifying content
|
|
*/
|
|
interface Requirements_Minifier {
|
|
|
|
/**
|
|
* Minify the given content
|
|
*
|
|
* @param string $content
|
|
* @param string $type Either js or css
|
|
* @param string $filename Name of file to display in case of error
|
|
* @return string minified content
|
|
*/
|
|
public function minify($content, $type, $filename);
|
|
}
|