mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #4796 from open-sausages/pulls/4.0/fix-generated-files
API Replace CacheGeneratedAssetHandler
This commit is contained in:
commit
684684e3fe
@ -25,9 +25,13 @@ Injector:
|
|||||||
type: prototype
|
type: prototype
|
||||||
# Image mechanism
|
# Image mechanism
|
||||||
Image_Backend: GDBackend
|
Image_Backend: GDBackend
|
||||||
|
# Requirements config
|
||||||
GeneratedAssetHandler:
|
GeneratedAssetHandler:
|
||||||
class: SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler
|
class: SilverStripe\Filesystem\Storage\FlysystemGeneratedAssetHandler
|
||||||
properties:
|
properties:
|
||||||
AssetStore: '%$AssetStore'
|
Filesystem: '%$FlysystemBackend'
|
||||||
Requirements_Minifier:
|
Requirements_Minifier:
|
||||||
class: SilverStripe\View\JSMinifier
|
class: SilverStripe\View\JSMinifier
|
||||||
|
Requirements_Backend:
|
||||||
|
properties:
|
||||||
|
AssetHandler: '%$GeneratedAssetHandler'
|
||||||
|
@ -289,7 +289,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
$existingRequirementsBackend = Requirements::backend();
|
$existingRequirementsBackend = Requirements::backend();
|
||||||
|
|
||||||
Config::inst()->update('Cookie', 'report_errors', false);
|
Config::inst()->update('Cookie', 'report_errors', false);
|
||||||
Requirements::set_backend(new Requirements_Backend());
|
Requirements::set_backend(Injector::inst()->create('Requirements_Backend'));
|
||||||
|
|
||||||
// Set callback to invoke prior to return
|
// Set callback to invoke prior to return
|
||||||
$onCleanup = function() use(
|
$onCleanup = function() use(
|
||||||
|
@ -102,22 +102,83 @@ by reducing HTTP requests.
|
|||||||
|
|
||||||
<div class="alert" markdown='1'>
|
<div class="alert" markdown='1'>
|
||||||
To make debugging easier in your local environment, combined files is disabled when running your application in `dev`
|
To make debugging easier in your local environment, combined files is disabled when running your application in `dev`
|
||||||
mode.
|
mode. You can re-enable dev combination by setting `Requirements_Backend.combine_in_dev` to true.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
By default it stores the generated file in the assets/ folder, but you can configure this by pointing the
|
### Configuring combined file storage
|
||||||
`Requirements.combined_files_folder` configuration setting to a specific folder.
|
|
||||||
|
|
||||||
**mysite/_config/app.yml**
|
In some situations or server configurations, it may be necessary to customise the behaviour of generated javascript
|
||||||
|
files in order to ensure that current files are served in requests.
|
||||||
:::yml
|
|
||||||
Requirements:
|
|
||||||
combined_files_folder: '_combined'
|
|
||||||
|
|
||||||
<div class="info" markdown='1'>
|
By default, files will be generated on demand in the format `assets/_combinedfiles/name-<hash>.js`,
|
||||||
If SilverStripe doesn't have permissions on your server to write these files it will default back to including them
|
where `<hash>` represents the hash of the source files used to generate that content. The default flysystem backend,
|
||||||
individually. SilverStripe **will not** rewrite your paths within the file.
|
as used by the `[api:AssetStore]` backend, is used for this storage, but it can be substituted for any
|
||||||
</div>
|
other backend.
|
||||||
|
|
||||||
|
You can also use any of the below options in order to tweak this behaviour:
|
||||||
|
|
||||||
|
* `Requirements.disable_flush_combined` - By default all combined files are deleted on flush.
|
||||||
|
If combined files are stored in source control, and thus updated manually, you might want to
|
||||||
|
turn this on to disable this behaviour.
|
||||||
|
* `Requirements_Backend.combine_hash_querystring` - By default the `<hash>` of the source files is appended to
|
||||||
|
the end of the combined file (prior to the file extension). If combined files are versioned in source control,
|
||||||
|
or running in a distributed environment (such as one where the newest version of a file may not always be
|
||||||
|
immediately available) then it may sometimes be necessary to disable this. When this is set to true, the hash
|
||||||
|
will instead be appended via a querystring parameter to enable cache busting, but not in the
|
||||||
|
filename itself. I.e. `assets/_combinedfiles/name.js?m=<hash>`
|
||||||
|
* `Requirements_Backend.default_combined_files_folder` - This defaults to `_combinedfiles`, and is the folder
|
||||||
|
within the configured asset backend that combined files will be stored in. If using a backend shared with
|
||||||
|
other systems, it is usually necessary to distinguish combined files from other assets.
|
||||||
|
* `Requirements_Backend.combine_in_dev` - By default combined files will not be combined except in test
|
||||||
|
or live environments. Turning this on will allow for pre-combining of files in development mode.
|
||||||
|
|
||||||
|
In some cases it may be necessary to create a new storage backend for combined files, if the default location
|
||||||
|
is not appropriate. Normally a single backend is used for all site assets, so a number of objects must be
|
||||||
|
replaced. For instance, the below will set a new set of dependencies to write to `mysite/javascript/combined`
|
||||||
|
|
||||||
|
|
||||||
|
:::yaml
|
||||||
|
---
|
||||||
|
Name: myrequirements
|
||||||
|
---
|
||||||
|
Requirements:
|
||||||
|
disable_flush_combined: true
|
||||||
|
Requirements_Backend:
|
||||||
|
combine_in_dev: true
|
||||||
|
combine_hash_querystring: true
|
||||||
|
default_combined_files_folder: 'combined'
|
||||||
|
Injector:
|
||||||
|
MySiteAdapter:
|
||||||
|
class: 'SilverStripe\Filesystem\Flysystem\AssetAdapter'
|
||||||
|
constructor:
|
||||||
|
Root: ./mysite/javascript
|
||||||
|
# Define the default filesystem
|
||||||
|
MySiteBackend:
|
||||||
|
class: 'League\Flysystem\Filesystem'
|
||||||
|
constructor:
|
||||||
|
Adapter: '%$MySiteAdapter'
|
||||||
|
calls:
|
||||||
|
PublicURLPlugin: [ addPlugin, [ %$FlysystemUrlPlugin ] ]
|
||||||
|
# Requirements config
|
||||||
|
MySiteAssetHandler:
|
||||||
|
class: SilverStripe\Filesystem\Storage\FlysystemGeneratedAssetHandler
|
||||||
|
properties:
|
||||||
|
Filesystem: '%$MySiteBackend'
|
||||||
|
Requirements_Backend:
|
||||||
|
properties:
|
||||||
|
AssetHandler: '%$MySiteAssetHandler'
|
||||||
|
|
||||||
|
In the above configuration, automatic expiry of generated files has been disabled, and it is necessary for
|
||||||
|
the developer to maintain these files manually. This may be useful in environments where assets must
|
||||||
|
be pre-cached, where scripts must be served alongside static files, or where no framework php request is
|
||||||
|
guaranteed. Alternatively, files may be served from instances other than the one which generated the
|
||||||
|
page response, and file synchronisation might not occur fast enough to propagate combined files to
|
||||||
|
mirrored filesystems.
|
||||||
|
|
||||||
|
In any case, care should be taken to determine the mechanism appropriate for your development
|
||||||
|
and production environments.
|
||||||
|
|
||||||
|
### Combined CSS Files
|
||||||
|
|
||||||
You can also combine CSS files into a media-specific stylesheets as you would with the `Requirements::css` call - use
|
You can also combine CSS files into a media-specific stylesheets as you would with the `Requirements::css` call - use
|
||||||
the third paramter of the `combine_files` function:
|
the third paramter of the `combine_files` function:
|
||||||
@ -130,6 +191,11 @@ the third paramter of the `combine_files` function:
|
|||||||
|
|
||||||
Requirements::combine_files('print.css', $printStylesheets, 'print');
|
Requirements::combine_files('print.css', $printStylesheets, 'print');
|
||||||
|
|
||||||
|
<div class="alert" markdown='1'>
|
||||||
|
When combining CSS files, take care of relative urls, as these will not be re-written to match
|
||||||
|
the destination location of the resulting combined CSS.
|
||||||
|
</div>
|
||||||
|
|
||||||
## Clearing assets
|
## Clearing assets
|
||||||
|
|
||||||
:::php
|
:::php
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
|
|
||||||
### ErrorPage
|
### ErrorPage
|
||||||
|
|
||||||
|
* `ErrorPage.static_filepath` config has been removed.
|
||||||
* `ErrorPage::get_filepath_for_errorcode` has been removed
|
* `ErrorPage::get_filepath_for_errorcode` has been removed
|
||||||
* `ErrorPage::alternateFilepathForErrorcode` extension point has been removed
|
* `ErrorPage::alternateFilepathForErrorcode` extension point has been removed
|
||||||
|
|
||||||
@ -123,7 +124,6 @@ New methods on `Requirements` are added to access these:
|
|||||||
And some methods on `Requirements` and `Requirements_Backend` have been removed as they are obsolete.
|
And some methods on `Requirements` and `Requirements_Backend` have been removed as they are obsolete.
|
||||||
|
|
||||||
* `delete_combined_files` (both classes)
|
* `delete_combined_files` (both classes)
|
||||||
* `delete_all_combined_files` (both classes)
|
|
||||||
|
|
||||||
A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be
|
A new config `Requirements_Backend.combine_in_dev` has been added in order to allow combined files to be
|
||||||
forced on during development. If this is off, combined files is only enabled in live environments.
|
forced on during development. If this is off, combined files is only enabled in live environments.
|
||||||
@ -418,27 +418,13 @@ After:
|
|||||||
|
|
||||||
### Update code that modifies the behaviour of ErrorPage
|
### Update code that modifies the behaviour of ErrorPage
|
||||||
|
|
||||||
Since ErrorPage writes statically cached files for each dataobject, in order to integrate it with both the
|
ErrorPage has been updated to use a configurable asset backend, similar to the `AssetStore` described above.
|
||||||
new asset backend, and ensure that .htaccess static references still works, these files are now cached
|
This replaces the `ErrorPage.static_filepath` config that was used to write local files.
|
||||||
potentially in two places:
|
|
||||||
|
|
||||||
* When an error is generated within the live environment, by default the error handler will query the
|
As a result, error pages may be cached either to a local filesystem, or an external Flysystem store
|
||||||
`GeneratedAssetHandler` for cached content, which is then served up in the same PHP request. This is the
|
(which is configured via setting a new Flysystem backend with YAML).
|
||||||
primary cache for error content, which does not involve database access, but is processed within the
|
|
||||||
PHP process. Although, the file path for this cache is not predictable, as it uses the configured asset backend,
|
|
||||||
and is not necessarily present on the same filesystem as the site code.
|
|
||||||
|
|
||||||
* In order to ensure that the webserver has direct access an available cached error page, it can be necessary
|
`ErrorPage::get_filepath_for_errorcode()` has been removed, because the local path for a specific code is
|
||||||
to ensure a physical file is present locally. By setting the `ErrorPage.enable_static_file` config,
|
|
||||||
the generation of this file can be controlled. When this disabled, the static file will only be cached
|
|
||||||
via the configured backend. When this is enabled (as it is by default) then the error page will be generated
|
|
||||||
in the same location as it were in framework version 3.x. E.g. `/assets/error-404.html`
|
|
||||||
|
|
||||||
If your webserver relies on static paths encoded in `.htaccess` to these files, then it's preferable to leave
|
|
||||||
this option on. If using a non-local filesystem, and another mechanism for intercepting webserver errors,
|
|
||||||
then it may be preferable to leave this off, meaning that the local assets folder is unnecessary.
|
|
||||||
|
|
||||||
`ErrorPage::get_filepath_for_errorcode` has been removed, because the local path for a specific code is
|
|
||||||
no longer assumed. Instead you should use `ErrorPage::get_content_for_errorcode` which retrieves the
|
no longer assumed. Instead you should use `ErrorPage::get_content_for_errorcode` which retrieves the
|
||||||
appropriate content for that error using one of the methods above.
|
appropriate content for that error using one of the methods above.
|
||||||
|
|
||||||
|
@ -32,9 +32,22 @@ class AssetAdapter extends Local {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public function __construct($root = null, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS) {
|
public function __construct($root = null, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS) {
|
||||||
|
// Get root path
|
||||||
|
if (!$root) {
|
||||||
|
// Empty root will set the path to assets
|
||||||
|
$root = ASSETS_PATH;
|
||||||
|
} elseif(strpos($root, './') === 0) {
|
||||||
|
// Substitute leading ./ with BASE_PATH
|
||||||
|
$root = BASE_PATH . substr($root, 1);
|
||||||
|
} elseif(strpos($root, '../') === 0) {
|
||||||
|
// Substitute leading ./ with parent of BASE_PATH, in case storage is outside of the webroot.
|
||||||
|
$root = dirname(BASE_PATH) . substr($root, 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Override permissions with config
|
// Override permissions with config
|
||||||
$permissions = \Config::inst()->get(get_class($this), 'file_permissions');
|
$permissions = \Config::inst()->get(get_class($this), 'file_permissions');
|
||||||
parent::__construct($root ?: ASSETS_PATH, $writeFlags, $linkHandling, $permissions);
|
|
||||||
|
parent::__construct($root, $writeFlags, $linkHandling, $permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
106
filesystem/flysystem/FlysystemGeneratedAssetHandler.php
Normal file
106
filesystem/flysystem/FlysystemGeneratedAssetHandler.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Filesystem\Storage;
|
||||||
|
|
||||||
|
use Config;
|
||||||
|
use Exception;
|
||||||
|
use League\Flysystem\Filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Flysystem implementation of GeneratedAssetHandler for storing generated content
|
||||||
|
*
|
||||||
|
* @package framework
|
||||||
|
* @subpackage filesystem
|
||||||
|
*/
|
||||||
|
class FlysystemGeneratedAssetHandler implements GeneratedAssetHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flysystem store for files
|
||||||
|
*
|
||||||
|
* @var Filesystem
|
||||||
|
*/
|
||||||
|
protected $assetStore = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign the asset backend
|
||||||
|
*
|
||||||
|
* @param Filesystem $store
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setFilesystem(Filesystem $store) {
|
||||||
|
$this->assetStore = $store;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the asset backend
|
||||||
|
*
|
||||||
|
* @return Filesystem
|
||||||
|
*/
|
||||||
|
public function getFilesystem() {
|
||||||
|
return $this->assetStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContentURL($filename, $callback = null) {
|
||||||
|
$result = $this->checkOrCreate($filename, $callback);
|
||||||
|
if($result) {
|
||||||
|
return $this
|
||||||
|
->getFilesystem()
|
||||||
|
->getPublicUrl($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent($filename, $callback = null) {
|
||||||
|
$result = $this->checkOrCreate($filename, $callback);
|
||||||
|
if($result) {
|
||||||
|
return $this
|
||||||
|
->getFilesystem()
|
||||||
|
->read($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the file exists or that the $callback provided was able to regenerate it.
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
* @param callable $callback
|
||||||
|
* @return bool Whether or not the file exists
|
||||||
|
* @throws Exception If an error has occurred during save
|
||||||
|
*/
|
||||||
|
protected function checkOrCreate($filename, $callback = null) {
|
||||||
|
// Check if there is an existing asset
|
||||||
|
if ($this->getFilesystem()->has($filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$callback) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke regeneration and save
|
||||||
|
$content = call_user_func($callback);
|
||||||
|
$this->setContent($filename, $content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContent($filename, $content) {
|
||||||
|
// Store content
|
||||||
|
$result = $this
|
||||||
|
->getFilesystem()
|
||||||
|
->put($filename, $content);
|
||||||
|
|
||||||
|
if(!$result) {
|
||||||
|
throw new Exception("Error regenerating file \"{$filename}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeContent($filename) {
|
||||||
|
if($this->getFilesystem()->has($filename)) {
|
||||||
|
$handler = $this->getFilesystem()->get($filename);
|
||||||
|
$handler->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,177 +0,0 @@
|
|||||||
<?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 = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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');
|
|
||||||
$lifetime = Config::inst()->get(__CLASS__, 'lifetime') ?: null; // map falsey to null (indefinite)
|
|
||||||
$cache->setLifetime($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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -11,37 +11,45 @@ namespace SilverStripe\Filesystem\Storage;
|
|||||||
interface GeneratedAssetHandler {
|
interface GeneratedAssetHandler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a filename and entropy, determine if a pre-generated file is valid. If this file is invalid
|
* Returns a URL to a generated asset, if one is available.
|
||||||
* or expired, invoke $callback to regenerate the content.
|
*
|
||||||
*
|
* Given a filename, determine if a file is available. If the file is unavailable,
|
||||||
* Returns a URL to the generated file
|
* and a callback is supplied, invoke it to regenerate the content.
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $filename
|
||||||
* @param mixed $entropy
|
|
||||||
* @param callable $callback To generate content. If none provided, url will only be returned
|
* @param callable $callback To generate content. If none provided, url will only be returned
|
||||||
* if there is valid content.
|
* if there is valid content.
|
||||||
* @return string URL to generated file
|
* @return string URL to generated file
|
||||||
*/
|
*/
|
||||||
public function getGeneratedURL($filename, $entropy = 0, $callback = null);
|
public function getContentURL($filename, $callback = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a filename and entropy, determine if a pre-generated file is valid. If this file is invalid
|
* Returns the content for a generated asset, if one is available.
|
||||||
* or expired, invoke $callback to regenerate the content.
|
*
|
||||||
|
* Given a filename, determine if a file is available. If the file is unavailable,
|
||||||
|
* and a callback is supplied, invoke it to regenerate the content.
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $filename
|
||||||
* @param mixed $entropy
|
|
||||||
* @param callable $callback To generate content. If none provided, content will only be returned
|
* @param callable $callback To generate content. If none provided, content will only be returned
|
||||||
* if there is valid content.
|
* if there is valid content.
|
||||||
* @return string Content for this generated file
|
* @return string Content for this generated file
|
||||||
*/
|
*/
|
||||||
public function getGeneratedContent($filename, $entropy = 0, $callback = null);
|
public function getContent($filename, $callback = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update content with new value
|
* Update content with new value
|
||||||
*
|
*
|
||||||
* @param string $filename
|
* @param string $filename
|
||||||
* @param mixed $entropy
|
|
||||||
* @param string $content Content to write to the backend
|
* @param string $content Content to write to the backend
|
||||||
*/
|
*/
|
||||||
public function updateContent($filename, $entropy, $content);
|
public function setContent($filename, $content);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any content under the given file.
|
||||||
|
*
|
||||||
|
* If $filename is a folder, it should delete all files underneath it also.
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
*/
|
||||||
|
public function removeContent($filename);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use SilverStripe\Filesystem\Flysystem\FlysystemAssetStore;
|
|||||||
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
|
use SilverStripe\Filesystem\Flysystem\FlysystemUrlPlugin;
|
||||||
use SilverStripe\Filesystem\Storage\AssetContainer;
|
use SilverStripe\Filesystem\Storage\AssetContainer;
|
||||||
use SilverStripe\Filesystem\Storage\AssetStore;
|
use SilverStripe\Filesystem\Storage\AssetStore;
|
||||||
use SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler;
|
use SilverStripe\Filesystem\Storage\FlysystemGeneratedAssetHandler;
|
||||||
|
|
||||||
class AssetStoreTest extends SapphireTest {
|
class AssetStoreTest extends SapphireTest {
|
||||||
|
|
||||||
@ -424,6 +424,12 @@ class AssetStoreTest_SpyStore extends FlysystemAssetStore {
|
|||||||
$backend->setFilesystem($filesystem);
|
$backend->setFilesystem($filesystem);
|
||||||
Injector::inst()->registerService($backend, 'AssetStore');
|
Injector::inst()->registerService($backend, 'AssetStore');
|
||||||
|
|
||||||
|
// Assign flysystem backend to generated asset handler at the same time
|
||||||
|
$generated = new FlysystemGeneratedAssetHandler();
|
||||||
|
$generated->setFilesystem($filesystem);
|
||||||
|
Injector::inst()->registerService($generated, 'GeneratedAssetHandler');
|
||||||
|
Requirements::backend()->setAssetHandler($generated);
|
||||||
|
|
||||||
// Disable legacy and set defaults
|
// Disable legacy and set defaults
|
||||||
Config::inst()->remove(get_class(new FlysystemAssetStore()), 'legacy_filenames');
|
Config::inst()->remove(get_class(new FlysystemAssetStore()), 'legacy_filenames');
|
||||||
Config::inst()->update('Director', 'alternate_base_url', '/');
|
Config::inst()->update('Director', 'alternate_base_url', '/');
|
||||||
@ -451,9 +457,6 @@ class AssetStoreTest_SpyStore extends FlysystemAssetStore {
|
|||||||
* Reset defaults for this store
|
* Reset defaults for this store
|
||||||
*/
|
*/
|
||||||
public static function reset() {
|
public static function reset() {
|
||||||
// Need flushing since it won't have any files left
|
|
||||||
CacheGeneratedAssetHandler::flush();
|
|
||||||
|
|
||||||
// Remove all files in this store
|
// Remove all files in this store
|
||||||
if(self::$basedir) {
|
if(self::$basedir) {
|
||||||
$path = self::base_path();
|
$path = self::base_path();
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use SilverStripe\Filesystem\Storage\CacheGeneratedAssetHandler;
|
|
||||||
/**
|
/**
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage tests
|
* @subpackage tests
|
||||||
@ -23,7 +22,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testExternalUrls() {
|
public function testExternalUrls() {
|
||||||
$backend = new Requirements_Backend;
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$backend->setCombinedFilesEnabled(true);
|
$backend->setCombinedFilesEnabled(true);
|
||||||
|
|
||||||
$backend->javascript('http://www.mydomain.com/test.js');
|
$backend->javascript('http://www.mydomain.com/test.js');
|
||||||
@ -72,7 +71,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
$backend->clearCombinedFiles();
|
$backend->clearCombinedFiles();
|
||||||
$backend->setCombinedFilesFolder('_combinedfiles');
|
$backend->setCombinedFilesFolder('_combinedfiles');
|
||||||
$backend->setMinifyCombinedJSFiles(false);
|
$backend->setMinifyCombinedJSFiles(false);
|
||||||
CacheGeneratedAssetHandler::flush();
|
Requirements::flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,10 +118,10 @@ class RequirementsTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCombinedJavascript() {
|
public function testCombinedJavascript() {
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupCombinedRequirements($backend);
|
$this->setupCombinedRequirements($backend);
|
||||||
|
|
||||||
$combinedFileName = '/_combinedfiles/b2a28d2463/RequirementsTest_bc.js';
|
$combinedFileName = '/_combinedfiles/RequirementsTest_bc-51622b5.js';
|
||||||
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
||||||
|
|
||||||
$html = $backend->includeInHTML(false, self::$html_template);
|
$html = $backend->includeInHTML(false, self::$html_template);
|
||||||
@ -167,7 +166,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
|
|
||||||
// Then do it again, this time not requiring the files beforehand
|
// Then do it again, this time not requiring the files beforehand
|
||||||
unlink($combinedFilePath);
|
unlink($combinedFilePath);
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupCombinedNonrequiredRequirements($backend);
|
$this->setupCombinedNonrequiredRequirements($backend);
|
||||||
$html = $backend->includeInHTML(false, self::$html_template);
|
$html = $backend->includeInHTML(false, self::$html_template);
|
||||||
|
|
||||||
@ -205,7 +204,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
|
|
||||||
public function testCombinedCss() {
|
public function testCombinedCss() {
|
||||||
$basePath = $this->getCurrentRelativePath();
|
$basePath = $this->getCurrentRelativePath();
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
|
|
||||||
$backend->combineFiles(
|
$backend->combineFiles(
|
||||||
@ -220,7 +219,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
$html = $backend->includeInHTML(false, self::$html_template);
|
$html = $backend->includeInHTML(false, self::$html_template);
|
||||||
|
|
||||||
$this->assertRegExp(
|
$this->assertRegExp(
|
||||||
'/href=".*\/print\.css/',
|
'/href=".*\/print\-94e723d\.css/',
|
||||||
$html,
|
$html,
|
||||||
'Print stylesheets have been combined.'
|
'Print stylesheets have been combined.'
|
||||||
);
|
);
|
||||||
@ -231,7 +230,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Test that combining a file multiple times doesn't trigger an error
|
// Test that combining a file multiple times doesn't trigger an error
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->combineFiles(
|
$backend->combineFiles(
|
||||||
'style.css',
|
'style.css',
|
||||||
@ -250,7 +249,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
|
|
||||||
$html = $backend->includeInHTML(false, self::$html_template);
|
$html = $backend->includeInHTML(false, self::$html_template);
|
||||||
$this->assertRegExp(
|
$this->assertRegExp(
|
||||||
'/href=".*\/style\.css/',
|
'/href=".*\/style\-2b3e4c9\.css/',
|
||||||
$html,
|
$html,
|
||||||
'Stylesheets have been combined.'
|
'Stylesheets have been combined.'
|
||||||
);
|
);
|
||||||
@ -258,9 +257,9 @@ class RequirementsTest extends SapphireTest {
|
|||||||
|
|
||||||
public function testBlockedCombinedJavascript() {
|
public function testBlockedCombinedJavascript() {
|
||||||
$basePath = $this->getCurrentRelativePath();
|
$basePath = $this->getCurrentRelativePath();
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupCombinedRequirements($backend);
|
$this->setupCombinedRequirements($backend);
|
||||||
$combinedFileName = '/_combinedfiles/b2a28d2463/RequirementsTest_bc.js';
|
$combinedFileName = '/_combinedfiles/RequirementsTest_bc-51622b5.js';
|
||||||
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
$combinedFilePath = AssetStoreTest_SpyStore::base_path() . $combinedFileName;
|
||||||
|
|
||||||
/* BLOCKED COMBINED FILES ARE NOT INCLUDED */
|
/* BLOCKED COMBINED FILES ARE NOT INCLUDED */
|
||||||
@ -279,7 +278,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
/* BLOCKED UNCOMBINED FILES ARE NOT INCLUDED */
|
/* BLOCKED UNCOMBINED FILES ARE NOT INCLUDED */
|
||||||
$this->setupCombinedRequirements($backend);
|
$this->setupCombinedRequirements($backend);
|
||||||
$backend->block($basePath .'/RequirementsTest_b.js');
|
$backend->block($basePath .'/RequirementsTest_b.js');
|
||||||
$combinedFileName2 = '/_combinedfiles/37bd2d9dcb/RequirementsTest_bc.js'; // SHA1 without file c included
|
$combinedFileName2 = '/_combinedfiles/RequirementsTest_bc-fc7468e.js'; // SHA1 without file c included
|
||||||
$combinedFilePath2 = AssetStoreTest_SpyStore::base_path() . $combinedFileName2;
|
$combinedFilePath2 = AssetStoreTest_SpyStore::base_path() . $combinedFileName2;
|
||||||
clearstatcache(); // needed to get accurate file_exists() results
|
clearstatcache(); // needed to get accurate file_exists() results
|
||||||
$html = $backend->includeInHTML(false, self::$html_template);
|
$html = $backend->includeInHTML(false, self::$html_template);
|
||||||
@ -315,7 +314,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
public function testArgsInUrls() {
|
public function testArgsInUrls() {
|
||||||
$basePath = $this->getCurrentRelativePath();
|
$basePath = $this->getCurrentRelativePath();
|
||||||
|
|
||||||
$backend = new Requirements_Backend;
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
|
|
||||||
$backend->javascript($basePath . '/RequirementsTest_a.js?test=1&test=2&test=3');
|
$backend->javascript($basePath . '/RequirementsTest_a.js?test=1&test=2&test=3');
|
||||||
@ -340,7 +339,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
public function testRequirementsBackend() {
|
public function testRequirementsBackend() {
|
||||||
$basePath = $this->getCurrentRelativePath();
|
$basePath = $this->getCurrentRelativePath();
|
||||||
|
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->javascript($basePath . '/a.js');
|
$backend->javascript($basePath . '/a.js');
|
||||||
|
|
||||||
@ -378,7 +377,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
// to something else
|
// to something else
|
||||||
$basePath = 'framework' . substr($basePath, strlen(FRAMEWORK_DIR));
|
$basePath = 'framework' . substr($basePath, strlen(FRAMEWORK_DIR));
|
||||||
|
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$holder = Requirements::backend();
|
$holder = Requirements::backend();
|
||||||
Requirements::set_backend($backend);
|
Requirements::set_backend($backend);
|
||||||
@ -407,7 +406,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testJsWriteToBody() {
|
public function testJsWriteToBody() {
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->javascript('http://www.mydomain.com/test.js');
|
$backend->javascript('http://www.mydomain.com/test.js');
|
||||||
|
|
||||||
@ -426,7 +425,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
|
|
||||||
public function testIncludedJsIsNotCommentedOut() {
|
public function testIncludedJsIsNotCommentedOut() {
|
||||||
$template = '<html><head></head><body><!--<script>alert("commented out");</script>--></body></html>';
|
$template = '<html><head></head><body><!--<script>alert("commented out");</script>--></body></html>';
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->javascript($this->getCurrentRelativePath() . '/RequirementsTest_a.js');
|
$backend->javascript($this->getCurrentRelativePath() . '/RequirementsTest_a.js');
|
||||||
$html = $backend->includeInHTML(false, $template);
|
$html = $backend->includeInHTML(false, $template);
|
||||||
@ -438,7 +437,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
public function testCommentedOutScriptTagIsIgnored() {
|
public function testCommentedOutScriptTagIsIgnored() {
|
||||||
$template = '<html><head></head><body><!--<script>alert("commented out");</script>-->'
|
$template = '<html><head></head><body><!--<script>alert("commented out");</script>-->'
|
||||||
. '<h1>more content</h1></body></html>';
|
. '<h1>more content</h1></body></html>';
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->setSuffixRequirements(false);
|
$backend->setSuffixRequirements(false);
|
||||||
$src = $this->getCurrentRelativePath() . '/RequirementsTest_a.js';
|
$src = $this->getCurrentRelativePath() . '/RequirementsTest_a.js';
|
||||||
@ -450,7 +449,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testForceJsToBottom() {
|
public function testForceJsToBottom() {
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
$backend->javascript('http://www.mydomain.com/test.js');
|
$backend->javascript('http://www.mydomain.com/test.js');
|
||||||
|
|
||||||
@ -506,7 +505,7 @@ class RequirementsTest extends SapphireTest {
|
|||||||
$template = '<html><head></head><body><header>My header</header><p>Body</p></body></html>';
|
$template = '<html><head></head><body><header>My header</header><p>Body</p></body></html>';
|
||||||
$basePath = $this->getCurrentRelativePath();
|
$basePath = $this->getCurrentRelativePath();
|
||||||
|
|
||||||
$backend = new Requirements_Backend();
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$this->setupRequirements($backend);
|
$this->setupRequirements($backend);
|
||||||
|
|
||||||
$backend->javascript($basePath .'/RequirementsTest_a.js');
|
$backend->javascript($basePath .'/RequirementsTest_a.js');
|
||||||
|
@ -146,7 +146,7 @@ class SSViewerTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testRequirementsCombine(){
|
public function testRequirementsCombine(){
|
||||||
$testBackend = new Requirements_Backend();
|
$testBackend = Injector::inst()->create('Requirements_Backend');
|
||||||
$testBackend->setSuffixRequirements(false);
|
$testBackend->setSuffixRequirements(false);
|
||||||
//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
|
//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ class SSViewerTest extends SapphireTest {
|
|||||||
$testBackend->processCombinedFiles();
|
$testBackend->processCombinedFiles();
|
||||||
$js = $testBackend->getJavascript();
|
$js = $testBackend->getJavascript();
|
||||||
$combinedTestFilePath = BASE_PATH . reset($js);
|
$combinedTestFilePath = BASE_PATH . reset($js);
|
||||||
$this->assertContains('testRequirementsCombine.js', $combinedTestFilePath);
|
$this->assertContains('_combinedfiles/testRequirementsCombine-7c20750.js', $combinedTestFilePath);
|
||||||
|
|
||||||
// and make sure the combined content matches the input content, i.e. no loss of functionality
|
// and make sure the combined content matches the input content, i.e. no loss of functionality
|
||||||
if(!file_exists($combinedTestFilePath)) {
|
if(!file_exists($combinedTestFilePath)) {
|
||||||
@ -1348,7 +1348,7 @@ after')
|
|||||||
$template = new SSViewer(array('SSViewerTestProcess'));
|
$template = new SSViewer(array('SSViewerTestProcess'));
|
||||||
$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
|
$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
|
||||||
|
|
||||||
$backend = new Requirements_Backend;
|
$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
$backend->setCombinedFilesEnabled(false);
|
$backend->setCombinedFilesEnabled(false);
|
||||||
$backend->combineFiles(
|
$backend->combineFiles(
|
||||||
'RequirementsTest_ab.css',
|
'RequirementsTest_ab.css',
|
||||||
|
@ -8,7 +8,28 @@ use SilverStripe\Filesystem\Storage\GeneratedAssetHandler;
|
|||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage view
|
* @subpackage view
|
||||||
*/
|
*/
|
||||||
class Requirements {
|
class Requirements implements Flushable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag whether combined files should be deleted on flush.
|
||||||
|
*
|
||||||
|
* By default all combined files are deleted on flush. If combined files are stored in source control,
|
||||||
|
* and thus updated manually, you might want to turn this on to disable this behaviour.
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $disable_flush_combined = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered early in the request when a flush is requested
|
||||||
|
*/
|
||||||
|
public static function flush() {
|
||||||
|
$disabled = Config::inst()->get(static::class, 'disable_flush_combined');
|
||||||
|
if(!$disabled) {
|
||||||
|
self::delete_all_combined_files();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable combining of css/javascript files.
|
* Enable combining of css/javascript files.
|
||||||
@ -66,6 +87,9 @@ class Requirements {
|
|||||||
*/
|
*/
|
||||||
private static $backend = null;
|
private static $backend = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Requirements_Backend
|
||||||
|
*/
|
||||||
public static function backend() {
|
public static function backend() {
|
||||||
if(!self::$backend) {
|
if(!self::$backend) {
|
||||||
self::$backend = Injector::inst()->create('Requirements_Backend');
|
self::$backend = Injector::inst()->create('Requirements_Backend');
|
||||||
@ -323,6 +347,14 @@ class Requirements {
|
|||||||
return self::backend()->getCombinedFiles();
|
return self::backend()->getCombinedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all generated combined files in the configured combined files directory,
|
||||||
|
* but doesn't delete the directory itself
|
||||||
|
*/
|
||||||
|
public static function delete_all_combined_files() {
|
||||||
|
return self::backend()->deleteAllCombinedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
|
* Re-sets the combined files definition. See {@link Requirements_Backend::clear_combined_files()}
|
||||||
*/
|
*/
|
||||||
@ -427,7 +459,8 @@ class Requirements {
|
|||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage view
|
* @subpackage view
|
||||||
*/
|
*/
|
||||||
class Requirements_Backend {
|
class Requirements_Backend
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to add caching query params to the requests for file-based requirements.
|
* Whether to add caching query params to the requests for file-based requirements.
|
||||||
@ -447,8 +480,10 @@ class Requirements_Backend {
|
|||||||
protected $combinedFilesEnabled = true;
|
protected $combinedFilesEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if files should be combined automatically on dev mode
|
* Determine if files should be combined automatically on dev mode.
|
||||||
* By default, files will be left uncombined when developing.
|
*
|
||||||
|
* By default combined files will not be combined except in test or
|
||||||
|
* live environments. Turning this on will allow for pre-combining of files in development mode.
|
||||||
*
|
*
|
||||||
* @config
|
* @config
|
||||||
* @var bool
|
* @var bool
|
||||||
@ -559,20 +594,53 @@ class Requirements_Backend {
|
|||||||
protected $forceJSToBottom = false;
|
protected $forceJSToBottom = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the default prefix for comined files
|
* Configures the default prefix for combined files.
|
||||||
|
*
|
||||||
|
* This defaults to `_combinedfiles`, and is the folder within the configured asset backend that
|
||||||
|
* combined files will be stored in. If using a backend shared with other systems, it is usually
|
||||||
|
* necessary to distinguish combined files from other assets.
|
||||||
*
|
*
|
||||||
* @config
|
* @config
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private static $default_combined_files_folder = '_combinedfiles';
|
private static $default_combined_files_folder = '_combinedfiles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to include the hash in the querystring instead of the filename for combined files.
|
||||||
|
*
|
||||||
|
* By default the `<hash>` of the source files is appended to the end of the combined file
|
||||||
|
* (prior to the file extension). If combined files are versioned in source control or running
|
||||||
|
* in a distributed environment (such as one where the newest version of a file may not always be
|
||||||
|
* immediately available) then it may sometimes be necessary to disable this. When this is set to true,
|
||||||
|
* the hash will instead be appended via a querystring parameter to enable cache busting, but not in
|
||||||
|
* the filename itself. I.e. `assets/_combinedfiles/name.js?m=<hash>`
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $combine_hash_querystring = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var GeneratedAssetHandler
|
||||||
|
*/
|
||||||
|
protected $assetHandler = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the backend storage for generated files
|
* Gets the backend storage for generated files
|
||||||
*
|
*
|
||||||
* @return GeneratedAssetHandler
|
* @return GeneratedAssetHandler
|
||||||
*/
|
*/
|
||||||
protected function getAssetHandler() {
|
public function getAssetHandler() {
|
||||||
return Injector::inst()->get('GeneratedAssetHandler');
|
return $this->assetHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new asset handler for this backend
|
||||||
|
*
|
||||||
|
* @param GeneratedAssetHandler $handler
|
||||||
|
*/
|
||||||
|
public function setAssetHandler(GeneratedAssetHandler $handler) {
|
||||||
|
$this->assetHandler = $handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -619,8 +687,8 @@ class Requirements_Backend {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the combined files folder prefix
|
* Retrieve the combined files folder prefix
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getCombinedFilesFolder() {
|
public function getCombinedFilesFolder() {
|
||||||
if($this->combinedFilesFolder) {
|
if($this->combinedFilesFolder) {
|
||||||
@ -931,7 +999,7 @@ class Requirements_Backend {
|
|||||||
$this->customCSS = $this->disabled['customCSS'];
|
$this->customCSS = $this->disabled['customCSS'];
|
||||||
$this->customHeadTags = $this->disabled['customHeadTags'];
|
$this->customHeadTags = $this->disabled['customHeadTags'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Block inclusion of a specific file
|
* Block inclusion of a specific file
|
||||||
*
|
*
|
||||||
@ -1229,7 +1297,7 @@ class Requirements_Backend {
|
|||||||
if(isset($this->combinedFiles[$combinedFileName])) {
|
if(isset($this->combinedFiles[$combinedFileName])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all files to necessary type list
|
// Add all files to necessary type list
|
||||||
$paths = array();
|
$paths = array();
|
||||||
$combinedType = null;
|
$combinedType = null;
|
||||||
@ -1270,7 +1338,7 @@ class Requirements_Backend {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->combinedFiles[$combinedFileName] = array(
|
$this->combinedFiles[$combinedFileName] = array(
|
||||||
'files' => $paths,
|
'files' => $paths,
|
||||||
'type' => $combinedType,
|
'type' => $combinedType,
|
||||||
@ -1328,6 +1396,16 @@ class Requirements_Backend {
|
|||||||
return $this->combinedFiles;
|
return $this->combinedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all combined files
|
||||||
|
*/
|
||||||
|
public function deleteAllCombinedFiles() {
|
||||||
|
$combinedFolder = $this->getCombinedFilesFolder();
|
||||||
|
if($combinedFolder) {
|
||||||
|
$this->getAssetHandler()->removeContent($combinedFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all registered CSS and JavaScript file combinations
|
* Clear all registered CSS and JavaScript file combinations
|
||||||
*/
|
*/
|
||||||
@ -1404,31 +1482,34 @@ class Requirements_Backend {
|
|||||||
* @return string URL to this resource
|
* @return string URL to this resource
|
||||||
*/
|
*/
|
||||||
protected function getCombinedFileURL($combinedFile, $fileList, $type) {
|
protected function getCombinedFileURL($combinedFile, $fileList, $type) {
|
||||||
// Generate path (Filename)
|
// Filter blocked files
|
||||||
$combinedFileID = File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
|
$fileList = array_diff($fileList, $this->getBlocked());
|
||||||
|
|
||||||
// Get entropy for this combined file (last modified date of most recent file)
|
// Generate path (Filename)
|
||||||
$entropy = $this->getEntropyOfFiles($fileList);
|
$hashQuerystring = Config::inst()->get(static::class, 'combine_hash_querystring');
|
||||||
|
if(!$hashQuerystring) {
|
||||||
|
$combinedFile = $this->hashedCombinedFilename($combinedFile, $fileList);
|
||||||
|
}
|
||||||
|
$combinedFileID = File::join_paths($this->getCombinedFilesFolder(), $combinedFile);
|
||||||
|
|
||||||
// Send file combination request to the backend, with an optional callback to perform regeneration
|
// Send file combination request to the backend, with an optional callback to perform regeneration
|
||||||
$minify = $this->getMinifyCombinedJSFiles();
|
$minify = $this->getMinifyCombinedJSFiles();
|
||||||
$combinedURL = $this
|
$combinedURL = $this
|
||||||
->getAssetHandler()
|
->getAssetHandler()
|
||||||
->getGeneratedURL(
|
->getContentURL(
|
||||||
$combinedFileID,
|
$combinedFileID,
|
||||||
$entropy,
|
|
||||||
function() use ($fileList, $minify, $type) {
|
function() use ($fileList, $minify, $type) {
|
||||||
// Physically combine all file content
|
// Physically combine all file content
|
||||||
$combinedData = '';
|
$combinedData = '';
|
||||||
$base = Director::baseFolder() . '/';
|
$base = Director::baseFolder() . '/';
|
||||||
$minifier = Injector::inst()->get('Requirements_Minifier');
|
$minifier = Injector::inst()->get('Requirements_Minifier');
|
||||||
foreach(array_diff($fileList, $this->getBlocked()) as $file) {
|
foreach($fileList as $file) {
|
||||||
$fileContent = file_get_contents($base . $file);
|
$fileContent = file_get_contents($base . $file);
|
||||||
// Use configured minifier
|
// Use configured minifier
|
||||||
if($minify) {
|
if($minify) {
|
||||||
$fileContent = $minifier->minify($fileContent, $type, $file);
|
$fileContent = $minifier->minify($fileContent, $type, $file);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->writeHeaderComment) {
|
if ($this->writeHeaderComment) {
|
||||||
// Write a header comment for each file for easier identification and debugging.
|
// Write a header comment for each file for easier identification and debugging.
|
||||||
$combinedData .= "/****** FILE: $file *****/\n";
|
$combinedData .= "/****** FILE: $file *****/\n";
|
||||||
@ -1439,15 +1520,31 @@ class Requirements_Backend {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the name isn't hashed, we will need to append the querystring m= parameter instead
|
||||||
// Since url won't be automatically suffixed, add it in here
|
// Since url won't be automatically suffixed, add it in here
|
||||||
if($this->getSuffixRequirements()) {
|
if($hashQuerystring && $this->getSuffixRequirements()) {
|
||||||
|
$hash = $this->hashOfFiles($fileList);
|
||||||
$q = stripos($combinedURL, '?') === false ? '?' : '&';
|
$q = stripos($combinedURL, '?') === false ? '?' : '&';
|
||||||
$combinedURL .= "{$q}m={$entropy}";
|
$combinedURL .= "{$q}m={$hash}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $combinedURL;
|
return $combinedURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a filename and list of files, generate a new filename unique to these files
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param array $files
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function hashedCombinedFilename($combinedFile, $fileList) {
|
||||||
|
$name = pathinfo($combinedFile, PATHINFO_FILENAME);
|
||||||
|
$hash = $this->hashOfFiles($fileList);
|
||||||
|
$extension = File::get_file_extension($combinedFile);
|
||||||
|
return $name . '-' . substr($hash, 0, 7) . '.' . $extension;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if combined files are enabled
|
* Check if combined files are enabled
|
||||||
*
|
*
|
||||||
@ -1482,20 +1579,20 @@ class Requirements_Backend {
|
|||||||
* any of these files have changed.
|
* any of these files have changed.
|
||||||
*
|
*
|
||||||
* @param array $fileList List of files
|
* @param array $fileList List of files
|
||||||
* @return int Last modified timestamp of these files
|
* @return string SHA1 bashed file hash
|
||||||
*/
|
*/
|
||||||
protected function getEntropyOfFiles($fileList) {
|
protected function hashOfFiles($fileList) {
|
||||||
// file exists, check modification date of every contained file
|
// Get hash based on hash of each file
|
||||||
$base = Director::baseFolder() . '/';
|
$base = Director::baseFolder() . '/';
|
||||||
$srcLastMod = 0;
|
$hash = '';
|
||||||
foreach($fileList as $file) {
|
foreach($fileList as $file) {
|
||||||
if(file_exists($base . $file)) {
|
if(file_exists($base . $file)) {
|
||||||
$srcLastMod = max(filemtime($base . $file), $srcLastMod);
|
$hash .= sha1_file($base . $file);
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidArgumentException("Combined file {$file} does not exist");
|
throw new InvalidArgumentException("Combined file {$file} does not exist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $srcLastMod;
|
return sha1($hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user