mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #4680 from open-sausages/features/dbfile-generated-files
API Generated files API
This commit is contained in:
commit
f252cfad20
@ -25,3 +25,7 @@ Injector:
|
||||
type: prototype
|
||||
# Image mechanism
|
||||
Image_Backend: GDBackend
|
||||
GeneratedAssetHandler:
|
||||
class: SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler
|
||||
properties:
|
||||
AssetStore: '%$AssetStore'
|
||||
|
@ -272,8 +272,9 @@ EOT
|
||||
// a more specific error description.
|
||||
if(Director::isLive() && $this->isError() && !$this->body) {
|
||||
$formatter = Injector::get('FriendlyErrorFormatter');
|
||||
$formatter->setStatusCode($this->statusCode);
|
||||
echo $formatter->format(array());
|
||||
echo $formatter->format(array(
|
||||
'code' => $this->statusCode
|
||||
));
|
||||
|
||||
} else {
|
||||
echo $this->body;
|
||||
|
@ -20,6 +20,8 @@
|
||||
the `DataObject::ID` in a `data-fileid` property, or via shortcodes. This is necessary because file
|
||||
urls are no longer able to identify assets.
|
||||
* Extension point `HtmlEditorField::processImage` has been removed, and moved to `Image::regenerateImageHTML`
|
||||
* `Requirements::combined_files` no longer does JS minification. Please ensure that JS files are optimised
|
||||
before combining.
|
||||
|
||||
## New API
|
||||
|
||||
@ -27,6 +29,8 @@
|
||||
* `ShortcodeHandler` interface to help generate standard handlers for HTML shortcodes in the editor.
|
||||
* `AssetNameGenerator` interface, including a `DefaultAssetNameGenerator` implementation, which is used to generate
|
||||
renaming suggestions based on an original given filename in order to resolve file duplication issues.
|
||||
* `GeneratedAssetHandler` API now used to store and manage generated files (such as those used for error page
|
||||
cache or combined files).
|
||||
|
||||
## Deprecated classes/methods
|
||||
|
||||
|
175
filesystem/storage/CacheGeneratedAssetHandler.php
Normal file
175
filesystem/storage/CacheGeneratedAssetHandler.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Filesystem\Storage;
|
||||
|
||||
use Config;
|
||||
use Exception;
|
||||
use Flushable;
|
||||
use SS_Cache;
|
||||
use Zend_Cache_Core;
|
||||
|
||||
/**
|
||||
* Handle references to generated files via cached tuples
|
||||
*
|
||||
* Important: If you are using the default FlysystemStore with legacy_filenames, you will need to ?flush
|
||||
* in order to refresh combined files.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
class CacheGeneratedAssetHandler implements GeneratedAssetHandler, Flushable {
|
||||
|
||||
/**
|
||||
* Lifetime of cache
|
||||
*
|
||||
* @config
|
||||
* @var int
|
||||
*/
|
||||
private static $lifetime = 0;
|
||||
|
||||
/**
|
||||
* Backend for generated files
|
||||
*
|
||||
* @var AssetStore
|
||||
*/
|
||||
protected $assetStore = null;
|
||||
|
||||
/**
|
||||
* Assign the asset backend
|
||||
*
|
||||
* @param AssetStore $store
|
||||
* @return $this
|
||||
*/
|
||||
public function setAssetStore(AssetStore $store) {
|
||||
$this->assetStore = $store;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the asset backend
|
||||
*
|
||||
* @return AssetStore
|
||||
*/
|
||||
public function getAssetStore() {
|
||||
return $this->assetStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Zend_Cache_Core
|
||||
*/
|
||||
protected static function get_cache() {
|
||||
$cache = SS_Cache::factory('CacheGeneratedAssetHandler');
|
||||
$cache->setLifetime(Config::inst()->get(__CLASS__, 'lifetime'));
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the cache
|
||||
*/
|
||||
public static function flush() {
|
||||
self::get_cache()->clean();
|
||||
}
|
||||
|
||||
public function getGeneratedURL($filename, $entropy = 0, $callback = null) {
|
||||
$result = $this->getGeneratedFile($filename, $entropy, $callback);
|
||||
if($result) {
|
||||
return $this
|
||||
->getAssetStore()
|
||||
->getAsURL($result['Filename'], $result['Hash'], $result['Variant']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getGeneratedContent($filename, $entropy = 0, $callback = null) {
|
||||
$result = $this->getGeneratedFile($filename, $entropy, $callback);
|
||||
if($result) {
|
||||
return $this
|
||||
->getAssetStore()
|
||||
->getAsString($result['Filename'], $result['Hash'], $result['Variant']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or return the tuple for the given file, optionally regenerating it if it
|
||||
* doesn't exist
|
||||
*
|
||||
* @param string $filename
|
||||
* @param mixed $entropy
|
||||
* @param callable $callback
|
||||
* @return array tuple array if available
|
||||
* @throws Exception If the file isn't available and $callback fails to regenerate content
|
||||
*/
|
||||
protected function getGeneratedFile($filename, $entropy = 0, $callback = null) {
|
||||
// Check if there is an existing asset
|
||||
$cache = self::get_cache();
|
||||
$cacheID = $this->getCacheKey($filename, $entropy);
|
||||
$data = $cache->load($cacheID);
|
||||
if($data) {
|
||||
$result = unserialize($data);
|
||||
$valid = $this->validateResult($result, $filename);
|
||||
if($valid) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate
|
||||
if($callback) {
|
||||
// Invoke regeneration and save
|
||||
$content = call_user_func($callback);
|
||||
return $this->updateContent($filename, $entropy, $content);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateContent($filename, $entropy, $content) {
|
||||
$cache = self::get_cache();
|
||||
$cacheID = $this->getCacheKey($filename, $entropy);
|
||||
|
||||
// Store content
|
||||
$result = $this
|
||||
->getAssetStore()
|
||||
->setFromString($content, $filename);
|
||||
if($result) {
|
||||
$cache->save(serialize($result), $cacheID);
|
||||
}
|
||||
|
||||
// Ensure this result is successfully saved
|
||||
$valid = $this->validateResult($result, $filename);
|
||||
if($valid) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
throw new Exception("Error regenerating file \"{$filename}\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for the given generated asset
|
||||
*
|
||||
* @param string $filename
|
||||
* @param mixed $entropy
|
||||
* @return string
|
||||
*/
|
||||
protected function getCacheKey($filename, $entropy = 0) {
|
||||
$cacheID = sha1($filename);
|
||||
if($entropy) {
|
||||
$cacheID .= '_' . sha1($entropy);
|
||||
}
|
||||
return $cacheID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the given result is valid
|
||||
*
|
||||
* @param mixed $result
|
||||
* @param string $filename
|
||||
* @return bool True if this $result is valid
|
||||
*/
|
||||
protected function validateResult($result, $filename) {
|
||||
if(!$result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retrieve URL from tuple
|
||||
$store = $this->getAssetStore();
|
||||
return $store->exists($result['Filename'], $result['Hash'], $result['Variant']);
|
||||
}
|
||||
|
||||
}
|
47
filesystem/storage/GeneratedAssetHandler.php
Normal file
47
filesystem/storage/GeneratedAssetHandler.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Filesystem\Storage;
|
||||
|
||||
/**
|
||||
* Interface to define a handler for persistent generated files
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
interface GeneratedAssetHandler {
|
||||
|
||||
/**
|
||||
* Given a filename and entropy, determine if a pre-generated file is valid. If this file is invalid
|
||||
* or expired, invoke $callback to regenerate the content.
|
||||
*
|
||||
* Returns a URL to the generated file
|
||||
*
|
||||
* @param string $filename
|
||||
* @param mixed $entropy
|
||||
* @param callable $callback To generate content. If none provided, url will only be returned
|
||||
* if there is valid content.
|
||||
* @return string URL to generated file
|
||||
*/
|
||||
public function getGeneratedURL($filename, $entropy = 0, $callback = null);
|
||||
|
||||
/**
|
||||
* Given a filename and entropy, determine if a pre-generated file is valid. If this file is invalid
|
||||
* or expired, invoke $callback to regenerate the content.
|
||||
*
|
||||
* @param string $filename
|
||||
* @param mixed $entropy
|
||||
* @param callable $callback To generate content. If none provided, content will only be returned
|
||||
* if there is valid content.
|
||||
* @return string Content for this generated file
|
||||
*/
|
||||
public function getGeneratedContent($filename, $entropy = 0, $callback = null);
|
||||
|
||||
/**
|
||||
* Update content with new value
|
||||
*
|
||||
* @param string $filename
|
||||
* @param mixed $entropy
|
||||
* @param string $content Content to write to the backend
|
||||
*/
|
||||
public function updateContent($filename, $entropy, $content);
|
||||
}
|
@ -2,89 +2,124 @@
|
||||
|
||||
namespace SilverStripe\Framework\Logging;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\AbstractProcessingHandler;
|
||||
use Monolog\Formatter\FormatterInterface;
|
||||
|
||||
/**
|
||||
* Produce a friendly error message
|
||||
*/
|
||||
class DebugViewFriendlyErrorFormatter implements FormatterInterface
|
||||
{
|
||||
class DebugViewFriendlyErrorFormatter implements FormatterInterface {
|
||||
|
||||
/**
|
||||
* Default status code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $statusCode = 500;
|
||||
|
||||
/**
|
||||
* Default friendly error
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $friendlyErrorMessage = 'Error';
|
||||
|
||||
/**
|
||||
* Default error body
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $friendlyErrorDetail;
|
||||
|
||||
/**
|
||||
* Get default status code
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode() {
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default status code
|
||||
*
|
||||
* @param int $statusCode
|
||||
*/
|
||||
public function setStatusCode($statusCode) {
|
||||
$this->statusCode = $statusCode;
|
||||
}
|
||||
|
||||
public function getTitle($title) {
|
||||
/**
|
||||
* Get friendly title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->friendlyErrorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set friendly title
|
||||
*
|
||||
* @param string $title
|
||||
*/
|
||||
public function setTitle($title) {
|
||||
$this->friendlyErrorMessage = $title;
|
||||
}
|
||||
|
||||
public function getBody($title) {
|
||||
/**
|
||||
* Get default error body
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBody() {
|
||||
return $this->friendlyErrorDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default error body
|
||||
*
|
||||
* @param string $body
|
||||
*/
|
||||
public function setBody($body) {
|
||||
$this->friendlyErrorDetail = $body;
|
||||
}
|
||||
|
||||
public function format(array $record)
|
||||
{
|
||||
|
||||
return $this->output();
|
||||
public function format(array $record) {
|
||||
// Get error code
|
||||
$code = empty($record['code']) ? $this->statusCode : $record['code'];
|
||||
return $this->output($code);
|
||||
}
|
||||
|
||||
public function formatBatch(array $records) {
|
||||
return $this->output();
|
||||
}
|
||||
$message = '';
|
||||
foreach ($records as $record) {
|
||||
$message .= $this->format($record);
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function output() {
|
||||
/**
|
||||
* Return the appropriate error content for the given status code
|
||||
*
|
||||
* @param int $statusCode
|
||||
* @return string Content in an appropriate format for the current request
|
||||
*/
|
||||
public function output($statusCode) {
|
||||
// TODO: Refactor into a content-type option
|
||||
if(\Director::is_ajax()) {
|
||||
return $this->friendlyErrorMessage;
|
||||
|
||||
} else {
|
||||
// TODO: Refactor this into CMS
|
||||
if(class_exists('ErrorPage')){
|
||||
$errorFilePath = \ErrorPage::get_filepath_for_errorcode(
|
||||
$this->statusCode,
|
||||
class_exists('Translatable') ? \Translatable::get_current_locale() : null
|
||||
);
|
||||
|
||||
if(file_exists($errorFilePath)) {
|
||||
$content = file_get_contents($errorFilePath);
|
||||
if(!headers_sent()) {
|
||||
header('Content-Type: text/html');
|
||||
}
|
||||
// $BaseURL is left dynamic in error-###.html, so that multi-domain sites don't get broken
|
||||
return str_replace('$BaseURL', \Director::absoluteBaseURL(), $content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$renderer = \Debug::create_debug_view();
|
||||
$output = $renderer->renderHeader();
|
||||
$output .= $renderer->renderInfo("Website Error", $this->friendlyErrorMessage, $this->friendlyErrorDetail);
|
||||
|
||||
if(\Email::config()->admin_email) {
|
||||
$mailto = \Email::obfuscate(\Email::config()->admin_email);
|
||||
$output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . '');
|
||||
}
|
||||
|
||||
$output .= $renderer->renderFooter();
|
||||
return $output;
|
||||
return $this->getTitle();
|
||||
}
|
||||
|
||||
$renderer = \Debug::create_debug_view();
|
||||
$output = $renderer->renderHeader();
|
||||
$output .= $renderer->renderInfo("Website Error", $this->getTitle(), $this->getBody());
|
||||
|
||||
if(\Email::config()->admin_email) {
|
||||
$mailto = \Email::obfuscate(\Email::config()->admin_email);
|
||||
$output .= $renderer->renderParagraph('Contact an administrator: ' . $mailto . '');
|
||||
}
|
||||
|
||||
$output .= $renderer->renderFooter();
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use SilverStripe\Filesystem\Flysystem\FlysystemAssetStore;
|
||||
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
|
||||
use SilverStripe\Filesystem\Storage\AssetContainer;
|
||||
use SilverStripe\Filesystem\Storage\AssetStore;
|
||||
use SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler;
|
||||
|
||||
class AssetStoreTest extends SapphireTest {
|
||||
|
||||
@ -450,6 +451,10 @@ class AssetStoreTest_SpyStore extends FlysystemAssetStore {
|
||||
* Reset defaults for this store
|
||||
*/
|
||||
public static function reset() {
|
||||
// Need flushing since it won't have any files left
|
||||
CacheGeneratedAssetHandler::flush();
|
||||
|
||||
// Remove all files in this store
|
||||
if(self::$basedir) {
|
||||
$path = self::base_path();
|
||||
if(file_exists($path)) {
|
||||
@ -469,6 +474,12 @@ class AssetStoreTest_SpyStore extends FlysystemAssetStore {
|
||||
if($asset instanceof Folder) {
|
||||
return self::base_path() . '/' . $asset->getFilename();
|
||||
}
|
||||
if($asset instanceof File) {
|
||||
$asset = $asset->File;
|
||||
}
|
||||
if($asset instanceof DBFile) {
|
||||
return BASE_PATH . $asset->getSourceURL();
|
||||
}
|
||||
return BASE_PATH . $asset->getUrl();
|
||||
}
|
||||
|
||||
|
@ -75,8 +75,8 @@ class HtmlEditorConfigTest extends SapphireTest {
|
||||
HtmlEditorConfig::require_js();
|
||||
$js = Requirements::get_custom_scripts();
|
||||
|
||||
$this->assertContains('tinymce.PluginManager.load("plugin1", "/mypath/plugin1");', $js);
|
||||
$this->assertContains('tinymce.PluginManager.load("plugin2", "/mypath/plugin2");', $js);
|
||||
$this->assertContains('tinymce.PluginManager.load("plugin1", "/mypath/plugin1");', $js['htmlEditorConfig']);
|
||||
$this->assertContains('tinymce.PluginManager.load("plugin2", "/mypath/plugin2");', $js['htmlEditorConfig']);
|
||||
}
|
||||
|
||||
public function testRequireJSIncludesAllConfigs() {
|
||||
@ -86,7 +86,7 @@ class HtmlEditorConfigTest extends SapphireTest {
|
||||
HtmlEditorConfig::require_js();
|
||||
$js = Requirements::get_custom_scripts();
|
||||
|
||||
$this->assertContains('"configA":{', $js);
|
||||
$this->assertContains('"configB":{', $js);
|
||||
$this->assertContains('"configA":{', $js['htmlEditorConfig']);
|
||||
$this->assertContains('"configB":{', $js['htmlEditorConfig']);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler;
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
@ -10,11 +12,19 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
static $html_template = '<html><head></head><body></body></html>';
|
||||
|
||||
static $old_requirements = null;
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
AssetStoreTest_SpyStore::activate('RequirementsTest'); // Set backend root to /RequirementsTest
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
AssetStoreTest_SpyStore::reset();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testExternalUrls() {
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$backend->setCombinedFilesEnabled(true);
|
||||
|
||||
$backend->javascript('http://www.mydomain.com/test.js');
|
||||
$backend->javascript('https://www.mysecuredomain.com/test.js');
|
||||
@ -51,23 +61,35 @@ class RequirementsTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup new backend
|
||||
*
|
||||
* @param Requirements_Backend $backend
|
||||
*/
|
||||
protected function setupRequirements($backend) {
|
||||
// Flush requirements
|
||||
$backend->clear();
|
||||
$backend->clearCombinedFiles();
|
||||
$backend->setCombinedFilesFolder('_combinedfiles');
|
||||
CacheGeneratedAssetHandler::flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup combined and non-combined js with the backend
|
||||
*
|
||||
* @param Requirements_Backend $backend
|
||||
*/
|
||||
protected function setupCombinedRequirements($backend) {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
$backend->clear();
|
||||
$backend->setCombinedFilesFolder('assets');
|
||||
|
||||
// clearing all previously generated requirements (just in case)
|
||||
$backend->clear_combined_files();
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
|
||||
$this->setupRequirements($backend);
|
||||
|
||||
// require files normally (e.g. called from a FormField instance)
|
||||
$backend->javascript($basePath . '/RequirementsTest_a.js');
|
||||
$backend->javascript($basePath . '/RequirementsTest_b.js');
|
||||
$backend->javascript($basePath . '/RequirementsTest_c.js');
|
||||
|
||||
// require two of those files as combined includes
|
||||
$backend->combine_files(
|
||||
$backend->combineFiles(
|
||||
'RequirementsTest_bc.js',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_b.js',
|
||||
@ -76,44 +98,46 @@ class RequirementsTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup combined files with the backend
|
||||
*
|
||||
* @param Requirements_Backend $backend
|
||||
*/
|
||||
protected function setupCombinedNonrequiredRequirements($backend) {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
$this->setupRequirements($backend);
|
||||
|
||||
$backend->clear();
|
||||
$backend->setCombinedFilesFolder('assets');
|
||||
|
||||
// clearing all previously generated requirements (just in case)
|
||||
$backend->clear_combined_files();
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
|
||||
// require files as combined includes
|
||||
$backend->combine_files(
|
||||
'RequirementsTest_bc.js',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_b.js',
|
||||
$basePath . '/RequirementsTest_c.js'
|
||||
)
|
||||
);
|
||||
}
|
||||
// require files as combined includes
|
||||
$backend->combineFiles(
|
||||
'RequirementsTest_bc.js',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_b.js',
|
||||
$basePath . '/RequirementsTest_c.js'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testCombinedJavascript() {
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$backend->setCombinedFilesFolder('assets');
|
||||
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupCombinedRequirements($backend);
|
||||
|
||||
$combinedFilePath = Director::baseFolder() . '/assets/' . 'RequirementsTest_bc.js';
|
||||
$combinedFileName = '/_combinedfiles/b2a28d2463/RequirementsTest_bc.js';
|
||||
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
/* COMBINED JAVASCRIPT FILE IS INCLUDED IN HTML HEADER */
|
||||
$this->assertTrue((bool)preg_match('/src=".*\/RequirementsTest_bc\.js/', $html),
|
||||
'combined javascript file is included in html header');
|
||||
$this->assertRegExp(
|
||||
'/src=".*' . preg_quote($combinedFileName, '/') . '/',
|
||||
$html,
|
||||
'combined javascript file is included in html header'
|
||||
);
|
||||
|
||||
/* COMBINED JAVASCRIPT FILE EXISTS */
|
||||
$this->assertTrue(file_exists($combinedFilePath),
|
||||
'combined javascript file exists');
|
||||
$this->assertTrue(
|
||||
file_exists($combinedFilePath),
|
||||
'combined javascript file exists'
|
||||
);
|
||||
|
||||
/* COMBINED JAVASCRIPT HAS CORRECT CONTENT */
|
||||
$this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false),
|
||||
@ -122,35 +146,42 @@ class RequirementsTest extends SapphireTest {
|
||||
'combined javascript has correct content');
|
||||
|
||||
/* COMBINED FILES ARE NOT INCLUDED TWICE */
|
||||
$this->assertFalse((bool)preg_match('/src=".*\/RequirementsTest_b\.js/', $html),
|
||||
'combined files are not included twice');
|
||||
$this->assertFalse((bool)preg_match('/src=".*\/RequirementsTest_c\.js/', $html),
|
||||
'combined files are not included twice');
|
||||
$this->assertNotRegExp(
|
||||
'/src=".*\/RequirementsTest_b\.js/',
|
||||
$html,
|
||||
'combined files are not included twice'
|
||||
);
|
||||
$this->assertNotRegExp(
|
||||
'/src=".*\/RequirementsTest_c\.js/',
|
||||
$html,
|
||||
'combined files are not included twice'
|
||||
);
|
||||
|
||||
/* NORMAL REQUIREMENTS ARE STILL INCLUDED */
|
||||
$this->assertTrue((bool)preg_match('/src=".*\/RequirementsTest_a\.js/', $html),
|
||||
'normal requirements are still included');
|
||||
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
$this->assertRegExp(
|
||||
'/src=".*\/RequirementsTest_a\.js/',
|
||||
$html,
|
||||
'normal requirements are still included'
|
||||
);
|
||||
|
||||
// Then do it again, this time not requiring the files beforehand
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$backend->setCombinedFilesFolder('assets');
|
||||
|
||||
unlink($combinedFilePath);
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupCombinedNonrequiredRequirements($backend);
|
||||
|
||||
$combinedFilePath = Director::baseFolder() . '/assets/' . 'RequirementsTest_bc.js';
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
/* COMBINED JAVASCRIPT FILE IS INCLUDED IN HTML HEADER */
|
||||
$this->assertTrue((bool)preg_match('/src=".*\/RequirementsTest_bc\.js/', $html),
|
||||
'combined javascript file is included in html header');
|
||||
$this->assertRegExp(
|
||||
'/src=".*' . preg_quote($combinedFileName, '/') . '/',
|
||||
$html,
|
||||
'combined javascript file is included in html header'
|
||||
);
|
||||
|
||||
/* COMBINED JAVASCRIPT FILE EXISTS */
|
||||
$this->assertTrue(file_exists($combinedFilePath),
|
||||
'combined javascript file exists');
|
||||
$this->assertTrue(
|
||||
file_exists($combinedFilePath),
|
||||
'combined javascript file exists'
|
||||
);
|
||||
|
||||
/* COMBINED JAVASCRIPT HAS CORRECT CONTENT */
|
||||
$this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false),
|
||||
@ -159,20 +190,24 @@ class RequirementsTest extends SapphireTest {
|
||||
'combined javascript has correct content');
|
||||
|
||||
/* COMBINED FILES ARE NOT INCLUDED TWICE */
|
||||
$this->assertFalse((bool)preg_match('/src=".*\/RequirementsTest_b\.js/', $html),
|
||||
'combined files are not included twice');
|
||||
$this->assertFalse((bool)preg_match('/src=".*\/RequirementsTest_c\.js/', $html),
|
||||
'combined files are not included twice');
|
||||
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
$this->assertNotRegExp(
|
||||
'/src=".*\/RequirementsTest_b\.js/',
|
||||
$html,
|
||||
'combined files are not included twice'
|
||||
);
|
||||
$this->assertNotRegExp(
|
||||
'/src=".*\/RequirementsTest_c\.js/',
|
||||
$html,
|
||||
'combined files are not included twice'
|
||||
);
|
||||
}
|
||||
|
||||
public function testCombinedCss() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
|
||||
$backend->combine_files(
|
||||
$backend->combineFiles(
|
||||
'print.css',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_print_a.css',
|
||||
@ -183,120 +218,156 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
$this->assertTrue((bool)preg_match('/href=".*\/print\.css/', $html), 'Print stylesheets have been combined.');
|
||||
$this->assertTrue((bool)preg_match(
|
||||
'/media="print/', $html),
|
||||
$this->assertRegExp(
|
||||
'/href=".*\/print\.css/',
|
||||
$html,
|
||||
'Print stylesheets have been combined.'
|
||||
);
|
||||
$this->assertRegExp(
|
||||
'/media="print/',
|
||||
$html,
|
||||
'Combined print stylesheet retains the media parameter'
|
||||
);
|
||||
|
||||
// Test that combining a file multiple times doesn't trigger an error
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$backend->combineFiles(
|
||||
'style.css',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_b.css',
|
||||
$basePath . '/RequirementsTest_c.css'
|
||||
)
|
||||
);
|
||||
$backend->combineFiles(
|
||||
'style.css',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_b.css',
|
||||
$basePath . '/RequirementsTest_c.css'
|
||||
)
|
||||
);
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
$this->assertRegExp(
|
||||
'/href=".*\/style\.css/',
|
||||
$html,
|
||||
'Stylesheets have been combined.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testBlockedCombinedJavascript() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$backend->setCombinedFilesFolder('assets');
|
||||
$combinedFilePath = Director::baseFolder() . '/assets/' . 'RequirementsTest_bc.js';
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupCombinedRequirements($backend);
|
||||
$combinedFileName = '/_combinedfiles/b2a28d2463/RequirementsTest_bc.js';
|
||||
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
||||
|
||||
/* BLOCKED COMBINED FILES ARE NOT INCLUDED */
|
||||
$this->setupCombinedRequirements($backend);
|
||||
$backend->block('RequirementsTest_bc.js');
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
|
||||
clearstatcache(); // needed to get accurate file_exists() results
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
$this->assertFalse((bool)preg_match('/src=".*\/RequirementsTest_bc\.js/', $html),
|
||||
'blocked combined files are not included ');
|
||||
$this->assertFileNotExists($combinedFilePath);
|
||||
$this->assertNotRegExp(
|
||||
'/src=".*\/RequirementsTest_bc\.js/',
|
||||
$html,
|
||||
'blocked combined files are not included'
|
||||
);
|
||||
$backend->unblock('RequirementsTest_bc.js');
|
||||
|
||||
/* BLOCKED UNCOMBINED FILES ARE NOT INCLUDED */
|
||||
$this->setupCombinedRequirements($backend);
|
||||
$backend->block($basePath .'/RequirementsTest_b.js');
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
$combinedFileName2 = '/_combinedfiles/37bd2d9dcb/RequirementsTest_bc.js'; // SHA1 without file c included
|
||||
$combinedFilePath2 = AssetStoreTest_SpyStore::base_path() . $combinedFileName2;
|
||||
clearstatcache(); // needed to get accurate file_exists() results
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
$this->assertFalse((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false),
|
||||
'blocked uncombined files are not included');
|
||||
$backend->unblock('RequirementsTest_b.js');
|
||||
$this->assertFileExists($combinedFilePath2);
|
||||
$this->assertTrue(
|
||||
strpos(file_get_contents($combinedFilePath2), "alert('b')") === false,
|
||||
'blocked uncombined files are not included'
|
||||
);
|
||||
$backend->unblock($basePath . '/RequirementsTest_b.js');
|
||||
|
||||
/* A SINGLE FILE CAN'T BE INCLUDED IN TWO COMBINED FILES */
|
||||
$this->setupCombinedRequirements($backend);
|
||||
clearstatcache(); // needed to get accurate file_exists() results
|
||||
|
||||
// This throws a notice-level error, so we prefix with @
|
||||
@$backend->combine_files(
|
||||
// Exception generated from including invalid file
|
||||
$this->setExpectedException(
|
||||
'InvalidArgumentException',
|
||||
sprintf(
|
||||
"Requirements_Backend::combine_files(): Already included file(s) %s in combined file '%s'",
|
||||
$basePath . '/RequirementsTest_c.js',
|
||||
'RequirementsTest_bc.js'
|
||||
)
|
||||
);
|
||||
$backend->combineFiles(
|
||||
'RequirementsTest_ac.js',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_a.js',
|
||||
$basePath . '/RequirementsTest_c.js'
|
||||
)
|
||||
);
|
||||
|
||||
$combinedFiles = $backend->get_combine_files();
|
||||
$this->assertEquals(
|
||||
array_keys($combinedFiles),
|
||||
array('RequirementsTest_bc.js'),
|
||||
"A single file can't be included in two combined files"
|
||||
);
|
||||
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
}
|
||||
|
||||
public function testArgsInUrls() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(true);
|
||||
$this->setupRequirements($backend);
|
||||
|
||||
$backend->javascript($basePath . '/RequirementsTest_a.js?test=1&test=2&test=3');
|
||||
$backend->css($basePath . '/RequirementsTest_a.css?test=1&test=2&test=3');
|
||||
$backend->delete_combined_files('RequirementsTest_bc.js');
|
||||
|
||||
$html = $backend->includeInHTML(false, self::$html_template);
|
||||
|
||||
/* Javascript has correct path */
|
||||
$this->assertTrue(
|
||||
(bool)preg_match('/src=".*\/RequirementsTest_a\.js\?m=\d\d+&test=1&test=2&test=3/',$html),
|
||||
'javascript has correct path');
|
||||
$this->assertRegExp(
|
||||
'/src=".*\/RequirementsTest_a\.js\?m=\d\d+&test=1&test=2&test=3/',
|
||||
$html,
|
||||
'javascript has correct path'
|
||||
);
|
||||
|
||||
/* CSS has correct path */
|
||||
$this->assertTrue(
|
||||
(bool)preg_match('/href=".*\/RequirementsTest_a\.css\?m=\d\d+&test=1&test=2&test=3/',$html),
|
||||
'css has correct path');
|
||||
$this->assertRegExp(
|
||||
'/href=".*\/RequirementsTest_a\.css\?m=\d\d+&test=1&test=2&test=3/',
|
||||
$html,
|
||||
'css has correct path'
|
||||
);
|
||||
}
|
||||
|
||||
public function testRequirementsBackend() {
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$backend->javascript($basePath . '/a.js');
|
||||
|
||||
$this->assertTrue(count($backend->get_javascript()) == 1,
|
||||
$this->assertTrue(count($backend->getJavascript()) == 1,
|
||||
"There should be only 1 file included in required javascript.");
|
||||
$this->assertTrue(in_array($basePath . '/a.js', $backend->get_javascript()),
|
||||
$this->assertTrue(in_array($basePath . '/a.js', $backend->getJavascript()),
|
||||
"a.js should be included in required javascript.");
|
||||
|
||||
$backend->javascript($basePath . '/b.js');
|
||||
$this->assertTrue(count($backend->get_javascript()) == 2,
|
||||
$this->assertTrue(count($backend->getJavascript()) == 2,
|
||||
"There should be 2 files included in required javascript.");
|
||||
|
||||
$backend->block($basePath . '/a.js');
|
||||
$this->assertTrue(count($backend->get_javascript()) == 1,
|
||||
$this->assertTrue(count($backend->getJavascript()) == 1,
|
||||
"There should be only 1 file included in required javascript.");
|
||||
$this->assertFalse(in_array($basePath . '/a.js', $backend->get_javascript()),
|
||||
$this->assertFalse(in_array($basePath . '/a.js', $backend->getJavascript()),
|
||||
"a.js should not be included in required javascript after it has been blocked.");
|
||||
$this->assertTrue(in_array($basePath . '/b.js', $backend->get_javascript()),
|
||||
$this->assertTrue(in_array($basePath . '/b.js', $backend->getJavascript()),
|
||||
"b.js should be included in required javascript.");
|
||||
|
||||
$backend->css($basePath . '/a.css');
|
||||
$this->assertTrue(count($backend->get_css()) == 1,
|
||||
$this->assertTrue(count($backend->getCSS()) == 1,
|
||||
"There should be only 1 file included in required css.");
|
||||
$this->assertArrayHasKey($basePath . '/a.css', $backend->get_css(),
|
||||
$this->assertArrayHasKey($basePath . '/a.css', $backend->getCSS(),
|
||||
"a.css should be in required css.");
|
||||
|
||||
$backend->block($basePath . '/a.css');
|
||||
$this->assertTrue(count($backend->get_css()) == 0,
|
||||
$this->assertTrue(count($backend->getCSS()) == 0,
|
||||
"There should be nothing in required css after file has been blocked.");
|
||||
}
|
||||
|
||||
@ -307,6 +378,7 @@ class RequirementsTest extends SapphireTest {
|
||||
$basePath = 'framework' . substr($basePath, strlen(FRAMEWORK_DIR));
|
||||
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$holder = Requirements::backend();
|
||||
Requirements::set_backend($backend);
|
||||
$data = new ArrayData(array(
|
||||
@ -335,16 +407,17 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
public function testJsWriteToBody() {
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$backend->javascript('http://www.mydomain.com/test.js');
|
||||
|
||||
// Test matching with HTML5 <header> tags as well
|
||||
$template = '<html><head></head><body><header>My header</header><p>Body</p></body></html>';
|
||||
|
||||
$backend->set_write_js_to_body(false);
|
||||
$backend->setWriteJavascriptToBody(false);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertContains('<head><script', $html);
|
||||
|
||||
$backend->set_write_js_to_body(true);
|
||||
$backend->setWriteJavascriptToBody(true);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotContains('<head><script', $html);
|
||||
$this->assertContains('</script></body>', $html);
|
||||
@ -353,6 +426,7 @@ class RequirementsTest extends SapphireTest {
|
||||
public function testIncludedJsIsNotCommentedOut() {
|
||||
$template = '<html><head></head><body><!--<script>alert("commented out");</script>--></body></html>';
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$backend->javascript($this->getCurrentRelativePath() . '/RequirementsTest_a.js');
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
//wiping out commented-out html
|
||||
@ -364,9 +438,10 @@ class RequirementsTest extends SapphireTest {
|
||||
$template = '<html><head></head><body><!--<script>alert("commented out");</script>-->'
|
||||
. '<h1>more content</h1></body></html>';
|
||||
$backend = new Requirements_Backend();
|
||||
$backend->set_suffix_requirements(false);
|
||||
$this->setupRequirements($backend);
|
||||
$backend->setSuffixRequirements(false);
|
||||
$src = $this->getCurrentRelativePath() . '/RequirementsTest_a.js';
|
||||
$urlSrc = Controller::join_links(Director::baseURL(), $src);
|
||||
$urlSrc = ControllerTest_ContainerController::join_links(Director::baseURL(), $src);
|
||||
$backend->javascript($src);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertEquals('<html><head></head><body><!--<script>alert("commented out");</script>-->'
|
||||
@ -375,6 +450,7 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
public function testForceJsToBottom() {
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($backend);
|
||||
$backend->javascript('http://www.mydomain.com/test.js');
|
||||
|
||||
// Test matching with HTML5 <header> tags as well
|
||||
@ -391,8 +467,8 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
// Test if the script is before the head tag, not before the body.
|
||||
// Expected: $JsInHead
|
||||
$backend->set_write_js_to_body(false);
|
||||
$backend->set_force_js_to_bottom(false);
|
||||
$backend->setWriteJavascriptToBody(false);
|
||||
$backend->setForceJSToBottom(false);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotEquals($JsInBody, $html);
|
||||
$this->assertNotEquals($JsAtEnd, $html);
|
||||
@ -400,16 +476,16 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
// Test if the script is before the first <script> tag, not before the body.
|
||||
// Expected: $JsInBody
|
||||
$backend->set_write_js_to_body(true);
|
||||
$backend->set_force_js_to_bottom(false);
|
||||
$backend->setWriteJavascriptToBody(true);
|
||||
$backend->setForceJSToBottom(false);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotEquals($JsAtEnd, $html);
|
||||
$this->assertEquals($JsInBody, $html);
|
||||
|
||||
// Test if the script is placed just before the closing bodytag, with write-to-body false.
|
||||
// Expected: $JsAtEnd
|
||||
$backend->set_write_js_to_body(false);
|
||||
$backend->set_force_js_to_bottom(true);
|
||||
$backend->setWriteJavascriptToBody(false);
|
||||
$backend->setForceJSToBottom(true);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotEquals($JsInHead, $html);
|
||||
$this->assertNotEquals($JsInBody, $html);
|
||||
@ -417,8 +493,8 @@ class RequirementsTest extends SapphireTest {
|
||||
|
||||
// Test if the script is placed just before the closing bodytag, with write-to-body true.
|
||||
// Expected: $JsAtEnd
|
||||
$backend->set_write_js_to_body(true);
|
||||
$backend->set_force_js_to_bottom(true);
|
||||
$backend->setWriteJavascriptToBody(true);
|
||||
$backend->setForceJSToBottom(true);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotEquals($JsInHead, $html);
|
||||
$this->assertNotEquals($JsInBody, $html);
|
||||
@ -429,21 +505,22 @@ class RequirementsTest extends SapphireTest {
|
||||
$template = '<html><head></head><body><header>My header</header><p>Body</p></body></html>';
|
||||
$basePath = $this->getCurrentRelativePath();
|
||||
|
||||
$backend = new Requirements_Backend;
|
||||
$backend = new Requirements_Backend();
|
||||
$this->setupRequirements($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);
|
||||
$backend->setSuffixRequirements(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);
|
||||
$backend->setSuffixRequirements(false);
|
||||
$html = $backend->includeInHTML(false, $template);
|
||||
$this->assertNotContains('RequirementsTest_a.js=', $html);
|
||||
$this->assertNotRegexp('/RequirementsTest_a\.js\?m=[\d]*"/', $html);
|
||||
@ -452,26 +529,15 @@ class RequirementsTest extends SapphireTest {
|
||||
$this->assertNotRegexp('/RequirementsTest_b\.css\?m=[\d]*&foo=bar&bla=blubb"/', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given backend includes the given files
|
||||
*
|
||||
* @param Requirements_Backend $backend
|
||||
* @param string $type js or css
|
||||
* @param array|string $files Files or list of files to check
|
||||
*/
|
||||
public function assertFileIncluded($backend, $type, $files) {
|
||||
$type = strtolower($type);
|
||||
switch (strtolower($type)) {
|
||||
case 'css':
|
||||
$method = 'get_css';
|
||||
$type = 'CSS';
|
||||
break;
|
||||
case 'js':
|
||||
case 'javascript':
|
||||
case 'script':
|
||||
$method = 'get_javascript';
|
||||
$type = 'JavaScript';
|
||||
break;
|
||||
}
|
||||
$includedFiles = $backend->$method();
|
||||
|
||||
// Workaround for inconsistent return formats
|
||||
if($method == 'get_javascript') {
|
||||
$includedFiles = array_combine(array_values($includedFiles), array_values($includedFiles));
|
||||
}
|
||||
$includedFiles = $this->getBackendFiles($backend, $type);
|
||||
|
||||
if(is_array($files)) {
|
||||
$failedMatches = array();
|
||||
@ -497,26 +563,7 @@ class RequirementsTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function assertFileNotIncluded($backend, $type, $files) {
|
||||
$type = strtolower($type);
|
||||
switch ($type) {
|
||||
case 'css':
|
||||
$method = 'get_css';
|
||||
$type = 'CSS';
|
||||
break;
|
||||
case 'js':
|
||||
case 'get_javascript':
|
||||
case 'script':
|
||||
$method = 'get_javascript';
|
||||
$type = 'JavaScript';
|
||||
break;
|
||||
}
|
||||
$includedFiles = $backend->$method();
|
||||
|
||||
// Workaround for inconsistent return formats
|
||||
if($method == 'get_javascript') {
|
||||
$includedFiles = array_combine(array_values($includedFiles), array_values($includedFiles));
|
||||
}
|
||||
|
||||
$includedFiles = $this->getBackendFiles($backend, $type);
|
||||
if(is_array($files)) {
|
||||
$failedMatches = array();
|
||||
foreach ($files as $file) {
|
||||
@ -539,4 +586,26 @@ class RequirementsTest extends SapphireTest {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get files of the given type from the backend
|
||||
*
|
||||
* @param Requirements_Backend $backend
|
||||
* @param string $type js or css
|
||||
* @return array
|
||||
*/
|
||||
protected function getBackendFiles($backend, $type) {
|
||||
$type = strtolower($type);
|
||||
switch (strtolower($type)) {
|
||||
case 'css':
|
||||
return $backend->getCSS();
|
||||
case 'js':
|
||||
case 'javascript':
|
||||
case 'script':
|
||||
$scripts = $backend->getJavascript();
|
||||
return array_combine(array_values($scripts), array_values($scripts));
|
||||
}
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,12 @@ class SSViewerTest extends SapphireTest {
|
||||
parent::setUp();
|
||||
Config::inst()->update('SSViewer', 'source_file_comments', false);
|
||||
Config::inst()->update('SSViewer_FromString', 'cache_template', false);
|
||||
AssetStoreTest_SpyStore::activate('SSViewerTest');
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
AssetStoreTest_SpyStore::reset();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,54 +144,6 @@ class SSViewerTest extends SapphireTest {
|
||||
<% require css($cssFile) %>");
|
||||
$this->assertFalse((bool)trim($template), "Should be no content in this return.");
|
||||
}
|
||||
|
||||
public function testRequirementsCombine(){
|
||||
$oldBackend = Requirements::backend();
|
||||
$testBackend = new Requirements_Backend();
|
||||
Requirements::set_backend($testBackend);
|
||||
$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
|
||||
|
||||
$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
|
||||
$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
|
||||
Requirements::combine_files('testRequirementsCombine.js', array($jsFile));
|
||||
require_once('thirdparty/jsmin/jsmin.php');
|
||||
|
||||
// first make sure that our test js file causes an exception to be thrown
|
||||
try{
|
||||
$content = JSMin::minify($content);
|
||||
Requirements::set_backend($oldBackend);
|
||||
$this->fail('JSMin did not throw exception on minify bad file: ');
|
||||
}catch(Exception $e){
|
||||
// exception thrown... good
|
||||
}
|
||||
|
||||
// secondly, make sure that requirements combine throws the correct warning, and only that warning
|
||||
@unlink($combinedTestFilePath);
|
||||
try{
|
||||
Requirements::process_combined_files();
|
||||
}catch(PHPUnit_Framework_Error_Warning $e){
|
||||
if(strstr($e->getMessage(), 'Failed to minify') === false){
|
||||
Requirements::set_backend($oldBackend);
|
||||
$this->fail('Requirements::process_combined_files raised a warning, which is good, but this is not the expected warning ("Failed to minify..."): '.$e);
|
||||
}
|
||||
}catch(Exception $e){
|
||||
Requirements::set_backend($oldBackend);
|
||||
$this->fail('Requirements::process_combined_files did not catch exception caused by minifying bad js file: '.$e);
|
||||
}
|
||||
|
||||
// and make sure the combined content matches the input content, i.e. no loss of functionality
|
||||
if(!file_exists($combinedTestFilePath)){
|
||||
Requirements::set_backend($oldBackend);
|
||||
$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
|
||||
}
|
||||
$combinedTestFileContents = file_get_contents($combinedTestFilePath);
|
||||
$this->assertContains($jsFileContents, $combinedTestFileContents);
|
||||
|
||||
// reset
|
||||
Requirements::set_backend($oldBackend);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testComments() {
|
||||
$output = $this->render(<<<SS
|
||||
@ -1357,8 +1315,8 @@ after')
|
||||
$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
|
||||
|
||||
$backend = new Requirements_Backend;
|
||||
$backend->set_combined_files_enabled(false);
|
||||
$backend->combine_files(
|
||||
$backend->setCombinedFilesEnabled(false);
|
||||
$backend->combineFiles(
|
||||
'RequirementsTest_ab.css',
|
||||
array(
|
||||
$basePath . '/RequirementsTest_a.css',
|
||||
|
@ -1,19 +1,14 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
|
||||
|
||||
/**
|
||||
* Requirements tracker for JavaScript and CSS.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage view
|
||||
*/
|
||||
class Requirements implements Flushable {
|
||||
|
||||
/**
|
||||
* Triggered early in the request when a flush is requested
|
||||
*/
|
||||
public static function flush() {
|
||||
self::delete_all_combined_files();
|
||||
}
|
||||
class Requirements {
|
||||
|
||||
/**
|
||||
* Enable combining of css/javascript files.
|
||||
@ -21,7 +16,7 @@ class Requirements implements Flushable {
|
||||
* @param bool $enable
|
||||
*/
|
||||
public static function set_combined_files_enabled($enable) {
|
||||
self::backend()->set_combined_files_enabled($enable);
|
||||
self::backend()->setCombinedFilesEnabled($enable);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,7 +25,7 @@ class Requirements implements Flushable {
|
||||
* @return bool
|
||||
*/
|
||||
public static function get_combined_files_enabled() {
|
||||
return self::backend()->get_combined_files_enabled();
|
||||
return self::backend()->getCombinedFilesEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,7 +46,7 @@ class Requirements implements Flushable {
|
||||
* @param bool
|
||||
*/
|
||||
public static function set_suffix_requirements($var) {
|
||||
self::backend()->set_suffix_requirements($var);
|
||||
self::backend()->setSuffixRequirements($var);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +55,7 @@ class Requirements implements Flushable {
|
||||
* @return bool
|
||||
*/
|
||||
public static function get_suffix_requirements() {
|
||||
return self::backend()->get_suffix_requirements();
|
||||
return self::backend()->getSuffixRequirements();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +68,7 @@ class Requirements implements Flushable {
|
||||
|
||||
public static function backend() {
|
||||
if(!self::$backend) {
|
||||
self::$backend = new Requirements_Backend();
|
||||
self::$backend = Injector::inst()->create('Requirements_Backend');
|
||||
}
|
||||
return self::$backend;
|
||||
}
|
||||
@ -112,7 +107,7 @@ class Requirements implements Flushable {
|
||||
* @return array
|
||||
*/
|
||||
public static function get_custom_scripts() {
|
||||
return self::backend()->get_custom_scripts();
|
||||
return self::backend()->getCustomScripts();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -224,7 +219,7 @@ class Requirements implements Flushable {
|
||||
* Removes all items from the block list
|
||||
*/
|
||||
public static function unblock_all() {
|
||||
self::backend()->unblock_all();
|
||||
self::backend()->unblockAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,7 +243,7 @@ class Requirements implements Flushable {
|
||||
* @param SS_HTTPResponse $response
|
||||
*/
|
||||
public static function include_in_response(SS_HTTPResponse $response) {
|
||||
return self::backend()->include_in_response($response);
|
||||
return self::backend()->includeInResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -273,12 +268,10 @@ class Requirements implements Flushable {
|
||||
* increases performance by fewer HTTP requests.
|
||||
*
|
||||
* The combined file is regenerated based on every file modification time. Optionally a
|
||||
* rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
|
||||
* JavaScript, we use the external JSMin library to minify the JavaScript.
|
||||
* 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. For easier debugging, we only minify JavaScript if not in development
|
||||
* mode ({@link Director::isDev()}).
|
||||
* 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
|
||||
@ -316,49 +309,32 @@ class Requirements implements Flushable {
|
||||
* @return bool|void
|
||||
*/
|
||||
public static function combine_files($combinedFileName, $files, $media = null) {
|
||||
self::backend()->combine_files($combinedFileName, $files, $media);
|
||||
self::backend()->combineFiles($combinedFileName, $files, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all combined files; keys are the combined file names, values are lists of
|
||||
* files being combined.
|
||||
* 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()->get_combine_files();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all dynamically generated combined files from the filesystem
|
||||
*
|
||||
* @param string $combinedFileName If left blank, all combined files are deleted.
|
||||
*/
|
||||
public static function delete_combined_files($combinedFileName = null) {
|
||||
return self::backend()->delete_combined_files($combinedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return self::backend()->delete_all_combined_files();
|
||||
return self::backend()->getCombinedFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
|
||||
*/
|
||||
public static function clear_combined_files() {
|
||||
self::backend()->clear_combined_files();
|
||||
self::backend()->clearCombinedFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
|
||||
* combined files.
|
||||
* Do the heavy lifting involved in combining the combined files.
|
||||
*/
|
||||
public static function process_combined_files() {
|
||||
return self::backend()->process_combined_files();
|
||||
return self::backend()->processCombinedFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -368,7 +344,7 @@ class Requirements implements Flushable {
|
||||
* @param bool
|
||||
*/
|
||||
public static function set_write_js_to_body($var) {
|
||||
self::backend()->set_write_js_to_body($var);
|
||||
self::backend()->setWriteJavascriptToBody($var);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -378,9 +354,28 @@ class Requirements implements Flushable {
|
||||
* @param boolean $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()->set_force_js_to_bottom($var);
|
||||
self::backend()->setForceJSToBottom($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -404,46 +399,57 @@ class Requirements_Backend {
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $suffix_requirements = true;
|
||||
protected $suffixRequirements = true;
|
||||
|
||||
/**
|
||||
* Whether to combine CSS and JavaScript files
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $combined_files_enabled = true;
|
||||
protected $combinedFilesEnabled = true;
|
||||
|
||||
/**
|
||||
* Determine if files should be combined automatically on dev mode
|
||||
* By default, files will be left uncombined when developing.
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $combine_in_dev = false;
|
||||
|
||||
/**
|
||||
* Paths to all required JavaScript files relative to docroot
|
||||
*
|
||||
* @var array $javascript
|
||||
* @var array
|
||||
*/
|
||||
protected $javascript = array();
|
||||
|
||||
/**
|
||||
* Paths to all required CSS files relative to the docroot.
|
||||
*
|
||||
* @var array $css
|
||||
* @var array
|
||||
*/
|
||||
protected $css = array();
|
||||
|
||||
/**
|
||||
* All custom javascript code that is inserted into the page's HTML
|
||||
*
|
||||
* @var array $customScript
|
||||
* @var array
|
||||
*/
|
||||
protected $customScript = array();
|
||||
|
||||
/**
|
||||
* All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag
|
||||
*
|
||||
* @var array $customCSS
|
||||
* @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();
|
||||
|
||||
@ -451,7 +457,7 @@ class Requirements_Backend {
|
||||
* Remembers the file paths or uniquenessIDs of all Requirements cleared through
|
||||
* {@link clear()}, so that they can be restored later.
|
||||
*
|
||||
* @var array $disabled
|
||||
* @var array
|
||||
*/
|
||||
protected $disabled = array();
|
||||
|
||||
@ -463,7 +469,7 @@ class Requirements_Backend {
|
||||
*
|
||||
* Use {@link unblock()} or {@link unblock_all()} to revert changes.
|
||||
*
|
||||
* @var array $blocked
|
||||
* @var array
|
||||
*/
|
||||
protected $blocked = array();
|
||||
|
||||
@ -471,23 +477,16 @@ class Requirements_Backend {
|
||||
* A list of combined files registered via {@link combine_files()}. Keys are the output file
|
||||
* names, values are lists of input files.
|
||||
*
|
||||
* @var array $combine_files
|
||||
* @var array
|
||||
*/
|
||||
public $combine_files = array();
|
||||
|
||||
/**
|
||||
* Use the JSMin library to minify any javascript file passed to {@link combine_files()}.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $combine_js_with_jsmin = true;
|
||||
protected $combinedFiles = array();
|
||||
|
||||
/**
|
||||
* Whether or not file headers should be written when combining files
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $write_header_comment = true;
|
||||
public $writeHeaderComment = true;
|
||||
|
||||
/**
|
||||
* Where to save combined files. By default they're placed in assets/_combinedfiles, however
|
||||
@ -505,22 +504,39 @@ class Requirements_Backend {
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $write_js_to_body = true;
|
||||
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 $force_js_to_bottom = false;
|
||||
protected $forceJSToBottom = false;
|
||||
|
||||
/**
|
||||
* Configures the default prefix for comined files
|
||||
*
|
||||
* @config
|
||||
* @var string
|
||||
*/
|
||||
private static $default_combined_files_folder = '_combinedfiles';
|
||||
|
||||
/**
|
||||
* Gets the backend storage for generated files
|
||||
*
|
||||
* @return GeneratedAssetHandler
|
||||
*/
|
||||
protected function getAssetHandler() {
|
||||
return Injector::inst()->get('GeneratedAssetHandler');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the combination of CSS and JavaScript files
|
||||
*
|
||||
* @param $enable
|
||||
* @param bool $enable
|
||||
*/
|
||||
public function set_combined_files_enabled($enable) {
|
||||
$this->combined_files_enabled = (bool) $enable;
|
||||
public function setCombinedFilesEnabled($enable) {
|
||||
$this->combinedFilesEnabled = (bool) $enable;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -528,15 +544,37 @@ class Requirements_Backend {
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_combined_files_enabled() {
|
||||
return $this->combined_files_enabled;
|
||||
public function getCombinedFilesEnabled() {
|
||||
return $this->combinedFilesEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder to save combined files in. By default they're placed in assets/_combinedfiles,
|
||||
* 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) {
|
||||
@ -544,10 +582,15 @@ class Requirements_Backend {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string Folder relative to the webroot
|
||||
* Retrieve the combined files folder prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCombinedFilesFolder() {
|
||||
return ($this->combinedFilesFolder) ? $this->combinedFilesFolder : ASSETS_DIR . '/_combinedfiles';
|
||||
if($this->combinedFilesFolder) {
|
||||
return $this->combinedFilesFolder;
|
||||
}
|
||||
return Config::inst()->get(__CLASS__, 'default_combined_files_folder');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -558,8 +601,8 @@ class Requirements_Backend {
|
||||
*
|
||||
* @param bool
|
||||
*/
|
||||
public function set_suffix_requirements($var) {
|
||||
$this->suffix_requirements = $var;
|
||||
public function setSuffixRequirements($var) {
|
||||
$this->suffixRequirements = $var;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -567,8 +610,8 @@ class Requirements_Backend {
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_suffix_requirements() {
|
||||
return $this->suffix_requirements;
|
||||
public function getSuffixRequirements() {
|
||||
return $this->suffixRequirements;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -576,18 +619,41 @@ class Requirements_Backend {
|
||||
* head tag.
|
||||
*
|
||||
* @param bool
|
||||
* @return $this
|
||||
*/
|
||||
public function set_write_js_to_body($var) {
|
||||
$this->write_js_to_body = $var;
|
||||
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 set_force_js_to_bottom($var) {
|
||||
$this->force_js_to_bottom = $var;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -600,23 +666,44 @@ class Requirements_Backend {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all required JavaScript
|
||||
* Remove a javascript requirement
|
||||
*
|
||||
* @param string $file
|
||||
*/
|
||||
protected function unsetJavascript($file) {
|
||||
unset($this->javascript[$file]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of required JavaScript, excluding blocked
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_javascript() {
|
||||
public function getJavascript() {
|
||||
return array_keys(array_diff_key($this->javascript, $this->blocked));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array_keys($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|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
||||
* @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;
|
||||
if($uniquenessID) {
|
||||
$this->customScript[$uniquenessID] = $script;
|
||||
} else {
|
||||
$this->customScript[] = $script;
|
||||
}
|
||||
|
||||
$script .= "\n";
|
||||
}
|
||||
@ -626,47 +713,63 @@ class Requirements_Backend {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_custom_scripts() {
|
||||
$requirements = "";
|
||||
|
||||
if($this->customScript) {
|
||||
foreach($this->customScript as $script) {
|
||||
$requirements .= "$script\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
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|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
||||
* @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;
|
||||
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|int $uniquenessID A unique ID that ensures a piece of code is only added once
|
||||
* @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;
|
||||
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[]|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
|
||||
* @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));
|
||||
@ -685,7 +788,7 @@ class Requirements_Backend {
|
||||
/**
|
||||
* Register the given stylesheet into the list of requirements.
|
||||
*
|
||||
* @param string $file The CSS file to load, relative to site root
|
||||
* @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')
|
||||
*/
|
||||
@ -695,13 +798,40 @@ class Requirements_Backend {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 get_css() {
|
||||
return array_diff_key($this->css, $this->blocked);
|
||||
public function getBlocked() {
|
||||
return $this->blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -745,6 +875,7 @@ class Requirements_Backend {
|
||||
$this->customCSS = $this->disabled['customCSS'];
|
||||
$this->customHeadTags = $this->disabled['customHeadTags'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Block inclusion of a specific file
|
||||
*
|
||||
@ -768,13 +899,13 @@ class Requirements_Backend {
|
||||
* @param string|int $fileOrID
|
||||
*/
|
||||
public function unblock($fileOrID) {
|
||||
if(isset($this->blocked[$fileOrID])) unset($this->blocked[$fileOrID]);
|
||||
unset($this->blocked[$fileOrID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all items from the block list
|
||||
*/
|
||||
public function unblock_all() {
|
||||
public function unblockAll() {
|
||||
$this->blocked = array();
|
||||
}
|
||||
|
||||
@ -797,26 +928,24 @@ class Requirements_Backend {
|
||||
$jsRequirements = '';
|
||||
|
||||
// Combine files - updates $this->javascript and $this->css
|
||||
$this->process_combined_files();
|
||||
$this->processCombinedFiles();
|
||||
|
||||
foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) {
|
||||
$path = Convert::raw2xml($this->path_for_file($file));
|
||||
foreach($this->getJavascript() as $file) {
|
||||
$path = Convert::raw2xml($this->pathForFile($file));
|
||||
if($path) {
|
||||
$jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Add all inline JavaScript *after* including external files they might rely on
|
||||
if($this->customScript) {
|
||||
foreach(array_diff_key($this->customScript,$this->blocked) as $script) {
|
||||
$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
|
||||
$jsRequirements .= "$script\n";
|
||||
$jsRequirements .= "\n//]]>\n</script>\n";
|
||||
}
|
||||
foreach($this->getCustomScripts() as $script) {
|
||||
$jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n";
|
||||
$jsRequirements .= "$script\n";
|
||||
$jsRequirements .= "\n//]]>\n</script>\n";
|
||||
}
|
||||
|
||||
foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
|
||||
$path = Convert::raw2xml($this->path_for_file($file));
|
||||
foreach($this->getCSS() as $file => $params) {
|
||||
$path = Convert::raw2xml($this->pathForFile($file));
|
||||
if($path) {
|
||||
$media = (isset($params['media']) && !empty($params['media']))
|
||||
? " media=\"{$params['media']}\"" : "";
|
||||
@ -824,15 +953,15 @@ class Requirements_Backend {
|
||||
}
|
||||
}
|
||||
|
||||
foreach(array_diff_key($this->customCSS, $this->blocked) as $css) {
|
||||
foreach($this->getCustomCSS() as $css) {
|
||||
$requirements .= "<style type=\"text/css\">\n$css\n</style>\n";
|
||||
}
|
||||
|
||||
foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) {
|
||||
foreach($this->getCustomHeadTags() as $customHeadTag) {
|
||||
$requirements .= "$customHeadTag\n";
|
||||
}
|
||||
|
||||
if ($this->force_js_to_bottom) {
|
||||
if ($this->getForceJSToBottom()) {
|
||||
// Remove all newlines from code to preserve layout
|
||||
$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
|
||||
|
||||
@ -842,7 +971,7 @@ class Requirements_Backend {
|
||||
|
||||
// Put CSS at the bottom of the head
|
||||
$content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content);
|
||||
} elseif($this->write_js_to_body) {
|
||||
} elseif($this->getWriteJavascriptToBody()) {
|
||||
// Remove all newlines from code to preserve layout
|
||||
$jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements);
|
||||
|
||||
@ -884,13 +1013,13 @@ class Requirements_Backend {
|
||||
*
|
||||
* @param SS_HTTPResponse $response
|
||||
*/
|
||||
public function include_in_response(SS_HTTPResponse $response) {
|
||||
$this->process_combined_files();
|
||||
public function includeInResponse(SS_HTTPResponse $response) {
|
||||
$this->processCombinedFiles();
|
||||
$jsRequirements = array();
|
||||
$cssRequirements = array();
|
||||
|
||||
foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) {
|
||||
$path = $this->path_for_file($file);
|
||||
foreach($this->getJavascript() as $file) {
|
||||
$path = $this->pathForFile($file);
|
||||
if($path) {
|
||||
$jsRequirements[] = str_replace(',', '%2C', $path);
|
||||
}
|
||||
@ -898,8 +1027,8 @@ class Requirements_Backend {
|
||||
|
||||
$response->addHeader('X-Include-JS', implode(',', $jsRequirements));
|
||||
|
||||
foreach(array_diff_key($this->css,$this->blocked) as $file => $params) {
|
||||
$path = $this->path_for_file($file);
|
||||
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;
|
||||
@ -948,13 +1077,17 @@ class Requirements_Backend {
|
||||
}
|
||||
} else {
|
||||
// Stub i18n implementation for when i18n is disabled.
|
||||
if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js';
|
||||
if(!$langOnly) {
|
||||
$files[] = FRAMEWORK_DIR . '/javascript/i18nx.js';
|
||||
}
|
||||
}
|
||||
|
||||
if($return) {
|
||||
return $files;
|
||||
} else {
|
||||
foreach($files as $file) $this->javascript($file);
|
||||
foreach($files as $file) {
|
||||
$this->javascript($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -964,15 +1097,16 @@ class Requirements_Backend {
|
||||
* @param string $fileOrUrl
|
||||
* @return string|bool
|
||||
*/
|
||||
protected function path_for_file($fileOrUrl) {
|
||||
if(preg_match('{^//|http[s]?}', $fileOrUrl)) {
|
||||
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->suffix_requirements) {
|
||||
if($this->getSuffixRequirements()) {
|
||||
$mtimesuffix = "?m=" . filemtime($filePath);
|
||||
$suffix = '&';
|
||||
}
|
||||
@ -996,13 +1130,10 @@ class Requirements_Backend {
|
||||
* increases performance by fewer HTTP requests.
|
||||
*
|
||||
* The combined file is regenerated based on every file modification time. Optionally a
|
||||
* rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are
|
||||
* JavaScript, we use the external JSMin library to minify the JavaScript. This can be
|
||||
* controlled using {@link $combine_js_with_jsmin}.
|
||||
* 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. For easier debugging, we only minify JavaScript if not in development
|
||||
* mode ({@link Director::isDev()}).
|
||||
* 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
|
||||
@ -1034,269 +1165,275 @@ class Requirements_Backend {
|
||||
* </code>
|
||||
*
|
||||
* @param string $combinedFileName Filename of the combined file relative to docroot
|
||||
* @param array $files Array of filenames relative to docroot
|
||||
* @param string $media
|
||||
*
|
||||
* @return bool|void
|
||||
* @param array $files Array of filenames relative to docroot
|
||||
* @param string $media If including CSS Files, you can specify a media type
|
||||
*/
|
||||
public function combine_files($combinedFileName, $files, $media = null) {
|
||||
// duplicate check
|
||||
foreach($this->combine_files as $_combinedFileName => $_files) {
|
||||
$duplicates = array_intersect($_files, $files);
|
||||
if($duplicates && $combinedFileName != $_combinedFileName) {
|
||||
user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates)
|
||||
. " in combined file '{$_combinedFileName}'", E_USER_NOTICE);
|
||||
return false;
|
||||
public function combineFiles($combinedFileName, $files, $media = null) {
|
||||
// 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, $media);
|
||||
break;
|
||||
case 'js':
|
||||
$this->javascript($path);
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
foreach($files as $index=>$file) {
|
||||
if(is_array($file)) {
|
||||
// Either associative array path=>path type=>type or numeric 0=>path 1=>type
|
||||
// Otherwise, assume path is the first item
|
||||
if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) {
|
||||
switch ($file['type']) {
|
||||
case 'css':
|
||||
$this->css($file['path'], $media);
|
||||
break;
|
||||
default:
|
||||
$this->javascript($file['path']);
|
||||
break;
|
||||
}
|
||||
$files[$index] = $file['path'];
|
||||
} elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) {
|
||||
switch ($file[1]) {
|
||||
case 'css':
|
||||
$this->css($file[0], $media);
|
||||
break;
|
||||
default:
|
||||
$this->javascript($file[0]);
|
||||
break;
|
||||
}
|
||||
$files[$index] = $file[0];
|
||||
} else {
|
||||
$file = array_shift($file);
|
||||
}
|
||||
}
|
||||
if (!is_array($file)) {
|
||||
if(substr($file, -2) == 'js') {
|
||||
$this->javascript($file);
|
||||
} elseif(substr($file, -3) == 'css') {
|
||||
$this->css($file, $media);
|
||||
} else {
|
||||
user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', "
|
||||
. "please specify by passing using an array instead.", E_USER_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
$this->combinedFiles[$combinedFileName] = array(
|
||||
'files' => $paths,
|
||||
'type' => $combinedType,
|
||||
'media' => $media
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
$this->combine_files[$combinedFileName] = $files;
|
||||
|
||||
// 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
|
||||
* files being combined.
|
||||
* associative arrays with 'files', 'type', and 'media' keys for details about this
|
||||
* combined file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_combine_files() {
|
||||
return $this->combine_files;
|
||||
public function getCombinedFiles() {
|
||||
return array_diff_key($this->combinedFiles, $this->blocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all dynamically generated combined files from the filesystem
|
||||
* Includes all combined files, including blocked ones
|
||||
*
|
||||
* @param string $combinedFileName If left blank, all combined files are deleted.
|
||||
* @return type
|
||||
*/
|
||||
public function delete_combined_files($combinedFileName = null) {
|
||||
$combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files;
|
||||
$combinedFolder = ($this->getCombinedFilesFolder()) ?
|
||||
(Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder();
|
||||
foreach($combinedFiles as $combinedFile => $sourceItems) {
|
||||
$filePath = $combinedFolder . '/' . $combinedFile;
|
||||
if(file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all generated combined files in the configured combined files directory,
|
||||
* but doesn't delete the directory itself.
|
||||
*/
|
||||
public function delete_all_combined_files() {
|
||||
$combinedFolder = $this->getCombinedFilesFolder();
|
||||
if(!$combinedFolder) return false;
|
||||
|
||||
$path = Director::baseFolder() . '/' . $combinedFolder;
|
||||
if(file_exists($path)) {
|
||||
Filesystem::removeFolder($path, true);
|
||||
}
|
||||
protected function getAllCombinedFiles() {
|
||||
return $this->combinedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered CSS and JavaScript file combinations
|
||||
*/
|
||||
public function clear_combined_files() {
|
||||
$this->combine_files = array();
|
||||
public function clearCombinedFiles() {
|
||||
$this->combinedFiles = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the
|
||||
* combined files.
|
||||
* Do the heavy lifting involved in combining the combined files.
|
||||
*/
|
||||
public function process_combined_files() {
|
||||
// The class_exists call prevents us loading SapphireTest.php (slow) just to know that
|
||||
// SapphireTest isn't running :-)
|
||||
if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test();
|
||||
else $runningTest = false;
|
||||
|
||||
if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) {
|
||||
public function processCombinedFiles() {
|
||||
// Check if combining is enabled
|
||||
if(!$this->enabledCombinedFiles()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make a map of files that could be potentially combined
|
||||
$combinerCheck = array();
|
||||
foreach($this->combine_files as $combinedFile => $sourceItems) {
|
||||
foreach($sourceItems as $sourceItem) {
|
||||
if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){
|
||||
user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " .
|
||||
"combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING);
|
||||
}
|
||||
$combinerCheck[$sourceItem] = $combinedFile;
|
||||
// Process each combined files
|
||||
foreach($this->getAllCombinedFiles() as $combinedFile => $combinedItem) {
|
||||
$fileList = $combinedItem['files'];
|
||||
$type = $combinedItem['type'];
|
||||
$media = $combinedItem['media'];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Work out the relative URL for the combined files from the base folder
|
||||
$combinedFilesFolder = ($this->getCombinedFilesFolder()) ? ($this->getCombinedFilesFolder() . '/') : '';
|
||||
|
||||
// Figure out which ones apply to this request
|
||||
$combinedFiles = array();
|
||||
$newJSRequirements = array();
|
||||
$newCSSRequirements = array();
|
||||
foreach($this->javascript as $file => $dummy) {
|
||||
if(isset($combinerCheck[$file])) {
|
||||
$newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = true;
|
||||
$combinedFiles[$combinerCheck[$file]] = true;
|
||||
} else {
|
||||
$newJSRequirements[$file] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach($this->css as $file => $params) {
|
||||
if(isset($combinerCheck[$file])) {
|
||||
// Inherit the parameters from the last file in the combine set.
|
||||
$newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params;
|
||||
$combinedFiles[$combinerCheck[$file]] = true;
|
||||
} else {
|
||||
$newCSSRequirements[$file] = $params;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the combined files
|
||||
$base = Director::baseFolder() . '/';
|
||||
foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) {
|
||||
$fileList = $this->combine_files[$combinedFile];
|
||||
$combinedFilePath = $base . $combinedFilesFolder . '/' . $combinedFile;
|
||||
|
||||
|
||||
// Make the folder if necessary
|
||||
if(!file_exists(dirname($combinedFilePath))) {
|
||||
Filesystem::makeFolder(dirname($combinedFilePath));
|
||||
// Generate this file, unless blocked
|
||||
$combinedURL = null;
|
||||
if(!isset($this->blocked[$combinedFile])) {
|
||||
$combinedURL = $this->getCombinedFileURL($combinedFile, $fileList);
|
||||
}
|
||||
|
||||
// If the file isn't writeable, don't even bother trying to make the combined file and return. The
|
||||
// files will be included individually instead. This is a complex test because is_writable fails
|
||||
// if the file doesn't exist yet.
|
||||
if((file_exists($combinedFilePath) && !is_writable($combinedFilePath))
|
||||
|| (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath)))
|
||||
) {
|
||||
user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
|
||||
E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine if we need to build the combined include
|
||||
if(file_exists($combinedFilePath)) {
|
||||
// file exists, check modification date of every contained file
|
||||
$srcLastMod = 0;
|
||||
foreach($fileList as $file) {
|
||||
if(file_exists($base . $file)) {
|
||||
$srcLastMod = max(filemtime($base . $file), $srcLastMod);
|
||||
// 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' => $media);
|
||||
$included = true;
|
||||
}
|
||||
// If already included, or otherwise blocked, then don't add into CSS
|
||||
}
|
||||
$this->css = $newCSS;
|
||||
break;
|
||||
}
|
||||
$refresh = $srcLastMod > filemtime($combinedFilePath);
|
||||
} else {
|
||||
// File doesn't exist, or refresh was explicitly required
|
||||
$refresh = true;
|
||||
}
|
||||
|
||||
if(!$refresh) continue;
|
||||
|
||||
$failedToMinify = false;
|
||||
$combinedData = "";
|
||||
foreach(array_diff($fileList, $this->blocked) as $file) {
|
||||
$fileContent = file_get_contents($base . $file);
|
||||
|
||||
try{
|
||||
$fileContent = $this->minifyFile($file, $fileContent);
|
||||
}catch(Exception $e){
|
||||
$failedToMinify = true;
|
||||
case 'js': {
|
||||
// Assoc array of file => true
|
||||
$newJS = array();
|
||||
foreach($this->getAllJavascript() as $script) {
|
||||
if(!in_array($script, $fileList)) {
|
||||
$newJS[$script] = true;
|
||||
} elseif(!$included && $combinedURL) {
|
||||
$newJS[$combinedURL] = true;
|
||||
$included = true;
|
||||
}
|
||||
// If already included, or otherwise blocked, then don't add into scripts
|
||||
}
|
||||
$this->javascript = $newJS;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->write_header_comment) {
|
||||
// Write a header comment for each file for easier identification and debugging. The semicolon between each file is required for jQuery to be combined properly and protects against unterminated statements.
|
||||
$combinedData .= "/****** FILE: $file *****/\n";
|
||||
}
|
||||
|
||||
$combinedData .= $fileContent . "\n";
|
||||
}
|
||||
|
||||
$successfulWrite = false;
|
||||
$fh = fopen($combinedFilePath, 'wb');
|
||||
if($fh) {
|
||||
if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true;
|
||||
fclose($fh);
|
||||
unset($fh);
|
||||
}
|
||||
|
||||
if($failedToMinify){
|
||||
// Failed to minify, use unminified files instead. This warning is raised at the end to allow code execution
|
||||
// to complete in case this warning is caught inside a try-catch block.
|
||||
user_error('Failed to minify '.$file.', exception: '.$e->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Unsuccessful write - just include the regular JS files, rather than the combined one
|
||||
if(!$successfulWrite) {
|
||||
user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'",
|
||||
E_USER_WARNING);
|
||||
continue;
|
||||
if($combinedURL && !$included) {
|
||||
throw new Exception("Failed to merge combined file {$combinedFile} with existing requirements");
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Alters the original information, which means you can't call this method repeatedly - it will behave
|
||||
// differently on the subsequent calls
|
||||
$this->javascript = $newJSRequirements;
|
||||
$this->css = $newCSSRequirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify the given $content according to the file type indicated in $filename
|
||||
* Given a set of files, combine them (as necessary) and return the url
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $content
|
||||
* @return string
|
||||
* @param string $combinedFile Filename for this combined file
|
||||
* @param array $fileList List of files to combine
|
||||
* @return string URL to this resource
|
||||
*/
|
||||
protected function minifyFile($filename, $content) {
|
||||
// if we have a javascript file and jsmin is enabled, minify the content
|
||||
$isJS = stripos($filename, '.js');
|
||||
if($isJS && $this->combine_js_with_jsmin) {
|
||||
require_once('thirdparty/jsmin/jsmin.php');
|
||||
protected function getCombinedFileURL($combinedFile, $fileList) {
|
||||
// Generate path (Filename)
|
||||
$combinedFileID = File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
|
||||
|
||||
increase_time_limit_to();
|
||||
$content = JSMin::minify($content);
|
||||
// Get entropy for this combined file (last modified date of most recent file)
|
||||
$entropy = $this->getEntropyOfFiles($fileList);
|
||||
|
||||
// Send file combination request to the backend, with an optional callback to perform regeneration
|
||||
$combinedURL = $this
|
||||
->getAssetHandler()
|
||||
->getGeneratedURL(
|
||||
$combinedFileID,
|
||||
$entropy,
|
||||
function() use ($fileList) {
|
||||
$combinedData = '';
|
||||
$base = Director::baseFolder() . '/';
|
||||
foreach(array_diff($fileList, $this->getBlocked()) as $file) {
|
||||
$fileContent = file_get_contents($base . $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;
|
||||
}
|
||||
);
|
||||
|
||||
// Since url won't be automatically suffixed, add it in here
|
||||
if($this->getSuffixRequirements()) {
|
||||
$q = stripos($combinedURL, '?') === false ? '?' : '&';
|
||||
$combinedURL .= "{$q}m={$entropy}";
|
||||
}
|
||||
$content .= ($isJS ? ';' : '') . "\n";
|
||||
return $content;
|
||||
|
||||
return $combinedURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if combined files are enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function enabledCombinedFiles() {
|
||||
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 int Last modified timestamp of these files
|
||||
*/
|
||||
protected function getEntropyOfFiles($fileList) {
|
||||
// file exists, check modification date of every contained file
|
||||
$base = Director::baseFolder() . '/';
|
||||
$srcLastMod = 0;
|
||||
foreach($fileList as $file) {
|
||||
if(file_exists($base . $file)) {
|
||||
$srcLastMod = max(filemtime($base . $file), $srcLastMod);
|
||||
} else {
|
||||
throw new InvalidArgumentException("Combined file {$file} does not exist");
|
||||
}
|
||||
}
|
||||
return $srcLastMod;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1340,7 +1477,7 @@ class Requirements_Backend {
|
||||
Debug::show($this->customCSS);
|
||||
Debug::show($this->customScript);
|
||||
Debug::show($this->customHeadTags);
|
||||
Debug::show($this->combine_files);
|
||||
Debug::show($this->combinedFiles);
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user