API Split SilverStripe\Assets into separate module

This commit is contained in:
Damian Mooyman 2017-03-16 16:47:26 +13:00
parent cfa6a36697
commit dae6d5902f
93 changed files with 7 additions and 13747 deletions

View File

@ -40,7 +40,7 @@ before_script:
- composer install --prefer-dist
- "if [ \"$DB\" = \"PGSQL\" ]; then composer require silverstripe/postgresql:2.0.x-dev --prefer-dist; fi"
- "if [ \"$DB\" = \"SQLITE\" ]; then composer require silverstripe/sqlite3:2.0.x-dev --prefer-dist; fi"
- composer require silverstripe/config:1.0.x-dev silverstripe/admin:1.0.x-dev --prefer-dist
- composer require silverstripe/config:1.0.x-dev silverstripe/admin:1.0.x-dev silverstripe/assets:1.0.x-dev --prefer-dist
- "if [ \"$CMS_TEST\" = \"1\" ]; then composer require silverstripe/cms:4.0.x-dev silverstripe/siteconfig:4.0.x-dev silverstripe/reports:4.0.x-dev --prefer-dist; fi"
- "if [ \"$CMS_TEST\" = \"1\" ]; then php ./cms/tests/bootstrap/mysite.php; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sh -e /etc/init.d/xvfb start; sleep 3; fi"
@ -49,14 +49,14 @@ before_script:
- "if [ \"$BEHAT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"1\" ]; then (vendor/bin/serve --bootstrap-file cms/tests/behat/serve-bootstrap.php &> serve.log &); fi"
script:
- "if [ \"$PHPUNIT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then vendor/bin/phpunit tests/php; fi"
- "if [ \"$PHPUNIT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then vendor/bin/phpunit; fi"
- "if [ \"$PHPUNIT_COVERAGE_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then phpdbg -qrr ./vendor/bin/phpunit --coverage-clover=coverage.xml; fi"
- "if [ \"$PHPUNIT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"1\" ]; then vendor/bin/phpunit cms/tests; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then vendor/bin/behat --config tests/behat/config.yml .; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"1\" ]; then vendor/bin/behat @cms --config tests/behat/cms-config.yml; fi"
- "if [ \"$PHPCS_TEST\" = \"1\" ]; then composer run-script lint; fi"
after_success:
- "if [ \"$PHPUNIT_COVERAGE_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then phpdbg -qrr ./vendor/bin/phpunit --coverage-clover=coverage.xml; fi"
- "if [ \"$PHPUNIT_COVERAGE_TEST\" = \"1\" ] && [ \"$CMS_TEST\" = \"\" ]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi"
after_failure:

View File

@ -182,38 +182,6 @@ mappings:
MemberImportForm: SilverStripe\Admin\MemberImportForm
ModelAdmin: SilverStripe\Admin\ModelAdmin
SecurityAdmin: SilverStripe\Admin\SecurityAdmin
SilverStripe\Filesystem\Storage\AssetContainer: SilverStripe\Assets\Storage\AssetContainer
SilverStripe\Filesystem\Storage\AssetNameGenerator: SilverStripe\Assets\Storage\AssetNameGenerator
SilverStripe\Filesystem\Storage\AssetStore: SilverStripe\Assets\Storage\AssetStore
SilverStripe\Filesystem\Storage\AssetStoreRouter: SilverStripe\Assets\Storage\AssetStoreRouter
SilverStripe\Filesystem\Storage\DBFile: SilverStripe\Assets\Storage\DBFile
SilverStripe\Filesystem\Storage\DefaultAssetNameGenerator: SilverStripe\Assets\Storage\DefaultAssetNameGenerator
SilverStripe\Filesystem\Storage\GeneratedAssetHandler: SilverStripe\Assets\Storage\GeneratedAssetHandler
SilverStripe\Filesystem\Storage\ProtectedFileController: SilverStripe\Assets\Storage\ProtectedFileController
SilverStripe\Filesystem\Flysystem\AssetAdapter: SilverStripe\Assets\Flysystem\AssetAdapter
SilverStripe\Filesystem\Flysystem\FlysystemAssetStore: SilverStripe\Assets\Flysystem\FlysystemAssetStore
SilverStripe\Filesystem\Storage\FlysystemGeneratedAssetHandler: SilverStripe\Assets\Flysystem\GeneratedAssetHandler
SilverStripe\Filesystem\Flysystem\ProtectedAdapter: SilverStripe\Assets\Flysystem\ProtectedAdapter
SilverStripe\Filesystem\Flysystem\ProtectedAssetAdapter: SilverStripe\Assets\Flysystem\ProtectedAssetAdapter
SilverStripe\Filesystem\Flysystem\PublicAdapter: SilverStripe\Assets\Flysystem\PublicAdapter
SilverStripe\Filesystem\Flysystem\PublicAssetAdapter: SilverStripe\Assets\Flysystem\PublicAssetAdapter
SilverStripe\Filesystem\AssetControlExtension: SilverStripe\Assets\AssetControlExtension
SilverStripe\Filesystem\AssetManipulationList: SilverStripe\Assets\AssetManipulationList
SilverStripe\Filesystem\Thumbnail: SilverStripe\Assets\Thumbnail
SilverStripe\Filesystem\ImageManipulation: SilverStripe\Assets\ImageManipulation
File: SilverStripe\Assets\File
SS_FileFinder: SilverStripe\Assets\FileFinder
SilverStripe\Assets\SS_FileFinder: SilverStripe\Assets\FileFinder
FileMigrationHelper: SilverStripe\Assets\FileMigrationHelper
FileNameFilter: SilverStripe\Assets\FileNameFilter
Filesystem: SilverStripe\Assets\Filesystem
Folder: SilverStripe\Assets\Folder
GDBackend: SilverStripe\Assets\GDBackend
Image: SilverStripe\Assets\Image
Image_Backend: SilverStripe\Assets\Image_Backend
ImagickBackend: SilverStripe\Assets\ImagickBackend
Upload: SilverStripe\Assets\Upload
Upload_Validator: SilverStripe\Assets\Upload_Validator
SilverStripe\Framework\Core\Configurable: SilverStripe\Core\Config\Configurable
PaginatedList: SilverStripe\ORM\PaginatedList
ArrayLib: SilverStripe\ORM\ArrayLib
@ -523,23 +491,6 @@ mappings:
ModelAdminTest_Contact: SilverStripe\Admin\Tests\ModelAdminTest\Contact
ModelAdminTest_Player: SilverStripe\Admin\Tests\ModelAdminTest\Player
SecurityAdminTest: SilverStripe\Admin\Tests\SecurityAdminTest
AssetControlExtensionTest: SilverStripe\Assets\Tests\AssetControlExtensionTest
AssetControlExtensionTest_VersionedObject: SilverStripe\Assets\Tests\VersionedObject
AssetControlExtensionTest_Object: SilverStripe\Assets\Tests\TestObject
AssetControlExtensionTest_ArchivedObject: SilverStripe\Assets\Tests\ArchivedObject
AssetManipulationListTest: SilverStripe\Assets\Tests\AssetManipulationListTest
FileFinderTest: SilverStripe\Assets\Tests\FileFinderTest
FileMigrationHelperTest: SilverStripe\Assets\Tests\FileMigrationHelperTest
FileMigrationHelperTest_Extension: SilverStripe\Assets\Tests\FileMigrationHelperTest\Extension
FileNameFilterTest: SilverStripe\Assets\Tests\FileNameFilterTest
FileTest: SilverStripe\Assets\Tests\FileTest
FileTest_MyCustomFile: SilverStripe\Assets\Tests\FileTest\MyCustomFile
FolderTest: SilverStripe\Assets\Tests\FolderTest
GDTest: SilverStripe\Assets\Tests\GDTest
GDBackend_ImageUnavailable: SilverStripe\Assets\Tests\GDTest\ImageUnavailable
ProtectedFileControllerTest: SilverStripe\Assets\Tests\ProtectedFileControllerTest
UploadTest: SilverStripe\Assets\Tests\UploadTest
UploadTest_Validator: SilverStripe\Assets\Tests\UploadTest\UploadTest_Validator
ControllerTest: SilverStripe\Control\Tests\ControllerTest
ControllerTest_Controller: SilverStripe\Control\Tests\ControllerTest\TestController
ControllerTest_UnsecuredController: SilverStripe\Control\Tests\ControllerTest\UnsecuredController
@ -663,10 +614,6 @@ mappings:
DateFieldViewJQueryTest: SilverStripe\Forms\Tests\DateFieldViewJQueryTest
DatetimeFieldTest: SilverStripe\Forms\Tests\DatetimeFieldTest
DatetimeFieldTest_Model: SilverStripe\Forms\Tests\DatetimeFieldTest\Model
DBFileTest: SilverStripe\Forms\Tests\DBFileTest
DBFileTest_Object: SilverStripe\Forms\Tests\DBFileTest\TestObject
DBFileTest_Subclass: SilverStripe\Forms\Tests\DBFileTest\Subclass
DBFileTest_ImageOnly: SilverStripe\Forms\Tests\DBFileTest\ImageOnly
DropdownFieldTest: SilverStripe\Forms\Tests\DropdownFieldTest
EmailFieldTest: SilverStripe\Forms\Tests\EmailFieldTest
EmailFieldTest_Validator: SilverStripe\Forms\Tests\EmailFieldTest\TestValidator
@ -842,15 +789,12 @@ mappings:
DBYearTest: SilverStripe\ORM\Tests\DBYearTest
DecimalTest: SilverStripe\ORM\Tests\DecimalTest
DecimalTest_DataObject: SilverStripe\ORM\Tests\DecimalTest\TestObject
GDImageTest: SilverStripe\ORM\Tests\GDImageTest
GroupedListTest: SilverStripe\ORM\Tests\GroupedListTest
HasManyListTest: SilverStripe\ORM\Tests\HasManyListTest
HierarchyTest: SilverStripe\ORM\Tests\HierarchyTest
HierarchyTest_Object: SilverStripe\ORM\Tests\HierarchyTest\TestObject
HierarchyHideTest_Object: SilverStripe\ORM\Tests\HierarchyTest\HideTestObject
HierarchyHideTest_SubObject: SilverStripe\ORM\Tests\HierarchyHideTest\HideTestSubObject
ImageTest: SilverStripe\ORM\Tests\ImageTest
ImagickImageTest: SilverStripe\ORM\Tests\ImagickImageTest
LabelFieldTest: SilverStripe\ORM\Tests\LabelFieldTest
ManyManyListExtensionTest: SilverStripe\ORM\Tests\ManyManyListExtensionTest
ManyManyListTest_IndirectPrimary: SilverStripe\ORM\Tests\ManyManyListTest\IndirectPrimary
@ -971,7 +915,6 @@ mappings:
ViewableDataTest_Cached: SilverStripe\View\Tests\ViewableDataTest\Cached
ViewableDataTest_NotCached: SilverStripe\View\Tests\ViewableDataTest\NotCached
ViewableDataTest_Failover: SilverStripe\View\Tests\ViewableDataTest\Failover
AssetStoreTest_SpyStore: SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore
GridField_URLHandlerTest: SilverStripe\Forms\Tests\GridField\GridField_URLHandlerTest
GridField_URLHandlerTest_Controller: SilverStripe\Forms\Tests\GridField\GridField_URLHandlerTest\TestController
GridField_URLHandlerTest_Component: SilverStripe\Forms\Tests\GridField\GridField_URLHandlerTest\TestComponent
@ -1046,7 +989,6 @@ mappings:
NamespacedClassManifestTest: SilverStripe\Core\Tests\Manifest\NamespacedClassManifestTest
ThemeResourceLoaderTest: SilverStripe\Core\Tests\Manifest\ThemeResourceLoaderTest
TokenisedRegularExpressionTest: SilverStripe\Core\Tests\Manifest\TokenisedRegularExpressionTest
AssetAdapterTest: SilverStripe\Assets\Tests\Flysystem\AssetAdapterTest
EmailTest: SilverStripe\Control\Tests\Email\EmailTest
EmailTest_Mailer: SilverStripe\Control\Tests\Email\EmailTest\TestMailer
MailerTest: SilverStripe\Control\Tests\Email\MailerTest

View File

@ -1,72 +1,4 @@
---
Name: coreflysystem
---
SilverStripe\Core\Injector\Injector:
# Define the default adapter for this filesystem
FlysystemPublicAdapter:
class: 'SilverStripe\Assets\Flysystem\PublicAssetAdapter'
# Define the secondary adapter for protected assets
FlysystemProtectedAdapter:
class: 'SilverStripe\Assets\Flysystem\ProtectedAssetAdapter'
# Define the default filesystem
FlysystemPublicBackend:
class: 'League\Flysystem\Filesystem'
constructor:
Adapter: '%$FlysystemPublicAdapter'
Config:
visibility: public
# Define the secondary filesystem for protected assets
FlysystemProtectedBackend:
class: 'League\Flysystem\Filesystem'
constructor:
Adapter: '%$FlysystemProtectedAdapter'
Config:
visibility: private
---
Name: coreassets
After:
- '#coreflysystem'
---
SilverStripe\Core\Injector\Injector:
# Define our SS asset backend
AssetStore:
class: 'SilverStripe\Assets\Flysystem\FlysystemAssetStore'
properties:
PublicFilesystem: '%$FlysystemPublicBackend'
ProtectedFilesystem: '%$FlysystemProtectedBackend'
ProtectedFileController:
class: SilverStripe\Assets\Storage\ProtectedFileController
properties:
RouteHandler: '%$AssetStore'
AssetNameGenerator:
class: SilverStripe\Assets\Storage\DefaultAssetNameGenerator
type: prototype
# Requirements config
GeneratedAssetHandler:
class: SilverStripe\Assets\Flysystem\GeneratedAssetHandler
properties:
Filesystem: '%$FlysystemPublicBackend'
SilverStripe\View\Requirements_Minifier:
class: SilverStripe\View\JSMinifier
SilverStripe\View\Requirements_Backend:
properties:
AssetHandler: '%$GeneratedAssetHandler'
---
Name: coreassetroutes
After:
- '#coreassets'
---
SilverStripe\Control\Director:
rules:
'assets': ProtectedFileController
---
Name: imageconfig
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Assets\Image_Backend:
class: SilverStripe\Assets\GDBackend
Image_Backend: '%$SilverStripe\Assets\Image_Backend'
---
Name: coreassetfield
---
SilverStripe\Core\Injector\Injector:

View File

@ -20,8 +20,6 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBDouble
Enum:
class: SilverStripe\ORM\FieldType\DBEnum
DBFile:
class: SilverStripe\Assets\Storage\DBFile
Float:
class: SilverStripe\ORM\FieldType\DBFloat
ForeignKey:

View File

@ -23,6 +23,7 @@
"monolog/monolog": "~1.11",
"nikic/php-parser": "^2 || ^3",
"silverstripe/config": "^1@dev",
"silverstripe/assets": "^1@dev",
"swiftmailer/swiftmailer": "~5.4",
"symfony/cache": "^3.3@dev",
"symfony/config": "^2.8",
@ -44,8 +45,6 @@
},
"autoload": {
"psr-4": {
"SilverStripe\\Assets\\": "src/Assets/",
"SilverStripe\\Assets\\Tests\\": "tests/php/Assets/",
"SilverStripe\\Control\\": "src/Control/",
"SilverStripe\\Control\\Tests\\": "tests/php/Control/",
"SilverStripe\\Core\\": "src/Core/",

View File

@ -20,6 +20,7 @@
<testsuite name="Default">
<directory>tests/php</directory>
<directory>silverstripe-assets/tests/php</directory>
</testsuite>
<listeners>
@ -37,9 +38,7 @@
<directory suffix=".php">.</directory>
<exclude>
<directory suffix=".php">thirdparty/</directory>
<directory suffix=".php">admin/thirdparty/</directory>
<directory suffix=".php">tests/php/</directory>
<directory suffix=".php">admin/tests/php/</directory>
</exclude>
</whitelist>
</filter>

View File

@ -1,304 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Security\Member;
/**
* This class provides the necessary business logic to ensure that any assets attached
* to a record are safely deleted, published, or protected during certain operations.
*
* This class will respect the canView() of each object, and will use it to determine
* whether or not public users can access attached assets. Public and live records
* will have their assets promoted to the public store.
*
* Assets which exist only on non-live stages will be protected.
*
* Assets which are no longer referenced will be flushed via explicit delete calls
* to the underlying filesystem.
*
* @property DataObject|Versioned $owner A {@see DataObject}, potentially decorated with {@see Versioned} extension.
*/
class AssetControlExtension extends DataExtension
{
/**
* When archiving versioned dataobjects, should assets be archived with them?
* If false, assets will be deleted when the dataobject is archived.
* If true, assets will be instead moved to the protected store, and can be
* restored when the dataobject is restored from archive.
*
* Note that this does not affect the archiving of the actual database record in any way,
* only the physical file.
*
* Unversioned dataobjects will ignore this option and always delete attached
* assets on deletion.
*
* @config
* @var bool
*/
private static $keep_archived_assets = false;
/**
* Ensure that deletes records remove their underlying file assets, without affecting
* other staged records.
*/
public function onAfterDelete()
{
// Prepare blank manipulation
$manipulations = new AssetManipulationList();
// Add all assets for deletion
$this->addAssetsFromRecord($manipulations, $this->owner, AssetManipulationList::STATE_DELETED);
// Whitelist assets that exist in other stages
$this->addAssetsFromOtherStages($manipulations);
// Apply visibility rules based on the final manipulation
$this->processManipulation($manipulations);
}
/**
* Ensure that changes to records flush overwritten files, and update the visibility
* of other assets.
*/
public function onBeforeWrite()
{
// Prepare blank manipulation
$manipulations = new AssetManipulationList();
// Mark overwritten object as deleted
if ($this->owner->isInDB()) {
$priorRecord = DataObject::get(get_class($this->owner))->byID($this->owner->ID);
if ($priorRecord) {
$this->addAssetsFromRecord($manipulations, $priorRecord, AssetManipulationList::STATE_DELETED);
}
}
// Add assets from new record with the correct visibility rules
$state = $this->getRecordState($this->owner);
$this->addAssetsFromRecord($manipulations, $this->owner, $state);
// Whitelist assets that exist in other stages
$this->addAssetsFromOtherStages($manipulations);
// Apply visibility rules based on the final manipulation
$this->processManipulation($manipulations);
}
/**
* Check default state of this record
*
* @param DataObject $record
* @return string One of AssetManipulationList::STATE_* constants
*/
protected function getRecordState($record)
{
if ($this->isVersioned()) {
// Check stage this record belongs to
$stage = $record->getSourceQueryParam('Versioned.stage') ?: Versioned::get_stage();
// Non-live stages are automatically non-public
if ($stage !== Versioned::LIVE) {
return AssetManipulationList::STATE_PROTECTED;
}
}
// Check if canView permits anonymous viewers
return Member::actAs(null, function () use ($record) {
return $record->canView()
? AssetManipulationList::STATE_PUBLIC
: AssetManipulationList::STATE_PROTECTED;
});
}
/**
* Given a set of asset manipulations, trigger any necessary publish, protect, or
* delete actions on each asset.
*
* @param AssetManipulationList $manipulations
*/
protected function processManipulation(AssetManipulationList $manipulations)
{
// When deleting from stage then check if we should archive assets
$archive = $this->owner->config()->get('keep_archived_assets');
// Publish assets
$this->publishAll($manipulations->getPublicAssets());
// Protect assets
$this->protectAll($manipulations->getProtectedAssets());
// Check deletion policy
$deletedAssets = $manipulations->getDeletedAssets();
if ($archive && $this->isVersioned()) {
// Archived assets are kept protected
$this->protectAll($deletedAssets);
} else {
// Otherwise remove all assets
$this->deleteAll($deletedAssets);
}
}
/**
* Checks all stages other than the current stage, and check the visibility
* of assets attached to those records.
*
* @param AssetManipulationList $manipulation Set of manipulations to add assets to
*/
protected function addAssetsFromOtherStages(AssetManipulationList $manipulation)
{
// Skip unversioned or unsaved assets
if (!$this->isVersioned() || !$this->owner->isInDB()) {
return;
}
// Unauthenticated member to use for checking visibility
$baseClass = $this->owner->baseClass();
$baseTable = $this->owner->baseTable();
$filter = array("\"{$baseTable}\".\"ID\"" => $this->owner->ID);
$stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages}
foreach ($stages as $stage) {
// Skip current stage; These should be handled explicitly
if ($stage === Versioned::get_stage()) {
continue;
}
// Check if record exists in this stage
$record = Versioned::get_one_by_stage($baseClass, $stage, $filter);
if (!$record) {
continue;
}
// Check visibility of this record, and record all attached assets
$state = $this->getRecordState($record);
$this->addAssetsFromRecord($manipulation, $record, $state);
}
}
/**
* Given a record, add all assets it contains to the given manipulation.
* State can be declared for this record, otherwise the underlying DataObject
* will be queried for canView() to see if those assets are public
*
* @param AssetManipulationList $manipulation Set of manipulations to add assets to
* @param DataObject $record Record
* @param string $state One of AssetManipulationList::STATE_* constant values.
*/
protected function addAssetsFromRecord(AssetManipulationList $manipulation, DataObject $record, $state)
{
// Find all assets attached to this record
$assets = $this->findAssets($record);
if (empty($assets)) {
return;
}
// Add all assets to this stage
foreach ($assets as $asset) {
$manipulation->addAsset($asset, $state);
}
}
/**
* Return a list of all tuples attached to this dataobject
* Note: Variants are excluded
*
* @param DataObject $record to search
* @return array
*/
protected function findAssets(DataObject $record)
{
// Search for dbfile instances
$files = array();
$fields = DataObject::getSchema()->fieldSpecs($record);
foreach ($fields as $field => $db) {
$fieldObj = $record->$field;
if (!($fieldObj instanceof DBFile)) {
continue;
}
// Omit variant and merge with set
$next = $record->dbObject($field)->getValue();
unset($next['Variant']);
if ($next) {
$files[] = $next;
}
}
// De-dupe
return array_map("unserialize", array_unique(array_map("serialize", $files)));
}
/**
* Determine if {@see Versioned) extension rules should be applied to this object
*
* @return bool
*/
protected function isVersioned()
{
return $this->owner->has_extension('SilverStripe\\ORM\\Versioning\\Versioned') && class_exists('SilverStripe\\ORM\\Versioning\\Versioned');
}
/**
* Delete all assets in the tuple list
*
* @param array $assets
*/
protected function deleteAll($assets)
{
if (empty($assets)) {
return;
}
$store = $this->getAssetStore();
foreach ($assets as $asset) {
$store->delete($asset['Filename'], $asset['Hash']);
}
}
/**
* Move all assets in the list to the public store
*
* @param array $assets
*/
protected function publishAll($assets)
{
if (empty($assets)) {
return;
}
$store = $this->getAssetStore();
foreach ($assets as $asset) {
$store->publish($asset['Filename'], $asset['Hash']);
}
}
/**
* Move all assets in the list to the protected store
*
* @param array $assets
*/
protected function protectAll($assets)
{
if (empty($assets)) {
return;
}
$store = $this->getAssetStore();
foreach ($assets as $asset) {
$store->protect($asset['Filename'], $asset['Hash']);
}
}
/**
* @return AssetStore
*/
protected function getAssetStore()
{
return Injector::inst()->get('AssetStore');
}
}

View File

@ -1,175 +0,0 @@
<?php
namespace SilverStripe\Assets;
/**
* Provides a mechanism for determining the effective visibility of a set of assets (identified by
* filename and hash), given their membership to objects of varying visibility.
*
* The effective visibility of assets is based on three rules:
* - If an asset is attached to any public record, that asset is public.
* - If an asset is not attached to any public record, but is attached to a protected record,
* that asset is protected.
* - If an asset is attached to a record being deleted, but not any existing public or protected
* record, then that asset is marked for deletion.
*
* Variants are ignored for the purpose of determining visibility
*/
class AssetManipulationList
{
const STATE_PUBLIC = 'public';
const STATE_PROTECTED = 'protected';
const STATE_DELETED = 'deleted';
/**
* List of public assets
*
* @var array
*/
protected $public = array();
/**
* List of protected assets
*
* @var array
*/
protected $protected = array();
/**
* List of deleted assets
*
* @var array
*/
protected $deleted = array();
/**
* Get an identifying key for a given filename and hash
*
* @param array $asset Asset tuple
* @return string
*/
protected function getAssetKey($asset)
{
return $asset['Hash'] . '/' . $asset['Filename'];
}
/**
* Add asset with the given state
*
* @param array $asset Asset tuple
* @param string $state One of the STATE_* const vars
* @return bool True if the asset was added to the set matching the given state
*/
public function addAsset($asset, $state)
{
switch ($state) {
case self::STATE_PUBLIC:
return $this->addPublicAsset($asset);
case self::STATE_PROTECTED:
return $this->addProtectedAsset($asset);
case self::STATE_DELETED:
return $this->addDeletedAsset($asset);
default:
throw new \InvalidArgumentException("Invalid state {$state}");
}
}
/**
* Mark a file as public
*
* @param array $asset Asset tuple
* @return bool True if the asset was added to the public set
*/
public function addPublicAsset($asset)
{
// Remove from protected / deleted lists
$key = $this->getAssetKey($asset);
unset($this->protected[$key]);
unset($this->deleted[$key]);
// Skip if already public
if (isset($this->public[$key])) {
return false;
}
unset($asset['Variant']);
$this->public[$key] = $asset;
return true;
}
/**
* Record an asset as protected
*
* @param array $asset Asset tuple
* @return bool True if the asset was added to the protected set
*/
public function addProtectedAsset($asset)
{
$key = $this->getAssetKey($asset);
// Don't demote from public
if (isset($this->public[$key])) {
return false;
}
unset($this->deleted[$key]);
// Skip if already protected
if (isset($this->protected[$key])) {
return false;
}
unset($asset['Variant']);
$this->protected[$key] = $asset;
return true;
}
/**
* Record an asset as deleted
*
* @param array $asset Asset tuple
* @return bool True if the asset was added to the deleted set
*/
public function addDeletedAsset($asset)
{
$key = $this->getAssetKey($asset);
// Only delete if this doesn't exist in any non-deleted state
if (isset($this->public[$key]) || isset($this->protected[$key])) {
return false;
}
// Skip if already deleted
if (isset($this->deleted[$key])) {
return false;
}
unset($asset['Variant']);
$this->deleted[$key] = $asset;
return true;
}
/**
* Get all public assets
*
* @return array
*/
public function getPublicAssets()
{
return $this->public;
}
/**
* Get protected assets
*
* @return array
*/
public function getProtectedAssets()
{
return $this->protected;
}
/**
* Get deleted assets
*
* @return array
*/
public function getDeletedAssets()
{
return $this->deleted;
}
}

View File

@ -1,1320 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Assets\Storage\AssetNameGenerator;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Convert;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\ReadonlyField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\View\Parsers\ShortcodeHandler;
use SilverStripe\View\Parsers\ShortcodeParser;
use InvalidArgumentException;
/**
* This class handles the representation of a file on the filesystem within the framework.
* Most of the methods also handle the {@link Folder} subclass.
*
* Note: The files are stored in the assets/ directory, but SilverStripe
* looks at the db object to gather information about a file such as URL
* It then uses this for all processing functions (like image manipulation).
*
* <b>Security</b>
*
* Caution: It is recommended to disable any script execution in the "assets/"
* directory in the webserver configuration, to reduce the risk of exploits.
* See http://doc.silverstripe.org/secure-development#filesystem
*
* <b>Asset storage</b>
*
* As asset storage is configured separately to any File DataObject records, this class
* does not make any assumptions about how these records are saved. They could be on
* a local filesystem, remote filesystem, or a virtual record container (such as in local memory).
*
* The File dataobject simply represents an externally facing view of shared resources
* within this asset store.
*
* Internally individual files are referenced by a"Filename" parameter, which represents a File, extension,
* and is optionally prefixed by a list of custom directories. This path is root-agnostic, so it does not
* automatically have a direct url mapping (even to the site's base directory).
*
* Additionally, individual files may have several versions distinguished by sha1 hash,
* of which a File DataObject can point to a single one. Files can also be distinguished by
* variants, which may be resized images or format-shifted documents.
*
* <b>Properties</b>
*
* - "Title": Optional title of the file (for display purposes only).
* Defaults to "Name". Note that the Title field of Folder (subclass of File)
* is linked to Name, so Name and Title will always be the same.
* -"File": Physical asset backing this DB record. This is a composite DB field with
* its own list of properties. {@see DBFile} for more information
* - "Content": Typically unused, but handy for a textual representation of
* files, e.g. for fulltext indexing of PDF documents.
* - "ParentID": Points to a {@link Folder} record. Should be in sync with
* "Filename". A ParentID=0 value points to the "assets/" folder, not the webroot.
* -"ShowInSearch": True if this file is searchable
*
* @property string $Name Basename of the file
* @property string $Title Title of the file
* @property DBFile $File asset stored behind this File record
* @property string $Content
* @property string $ShowInSearch Boolean that indicates if file is shown in search. Doesn't apply to Folders
* @property int $ParentID ID of parent File/Folder
* @property int $OwnerID ID of Member who owns the file
*
* @method File Parent() Returns parent File
* @method Member Owner() Returns Member object of file owner.
*
* @mixin Hierarchy
* @mixin Versioned
*/
class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumbnail, CMSPreviewable
{
use ImageManipulation;
private static $default_sort = "\"Name\"";
/**
* @config
* @var string
*/
private static $singular_name = "File";
private static $plural_name = "Files";
/**
* Permissions necessary to view files outside of the live stage (e.g. archive / draft stage).
*
* @config
* @var array
*/
private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_AssetAdmin', 'VIEW_DRAFT_CONTENT');
private static $db = array(
"Name" => "Varchar(255)",
"Title" => "Varchar(255)",
"File" => "DBFile",
// Only applies to files, doesn't inherit for folder
'ShowInSearch' => 'Boolean(1)',
);
private static $has_one = array(
"Parent" => "SilverStripe\\Assets\\File",
"Owner" => "SilverStripe\\Security\\Member"
);
private static $defaults = array(
"ShowInSearch" => 1,
);
private static $extensions = array(
"SilverStripe\\ORM\\Hierarchy\\Hierarchy",
"SilverStripe\\ORM\\Versioning\\Versioned"
);
private static $casting = array (
'TreeTitle' => 'HTMLFragment'
);
private static $table_name = 'File';
/**
* @config
* @var array List of allowed file extensions, enforced through {@link validate()}.
*
* Note: if you modify this, you should also change a configuration file in the assets directory.
* Otherwise, the files will be able to be uploaded but they won't be able to be served by the
* webserver.
*
* - If you are running Apache you will need to change assets/.htaccess
* - If you are running IIS you will need to change assets/web.config
*
* Instructions for the change you need to make are included in a comment in the config file.
*/
private static $allowed_extensions = array(
'', 'ace', 'arc', 'arj', 'asf', 'au', 'avi', 'bmp', 'bz2', 'cab', 'cda', 'css', 'csv', 'dmg', 'doc',
'docx', 'dotx', 'dotm', 'flv', 'gif', 'gpx', 'gz', 'hqx', 'ico', 'jar', 'jpeg', 'jpg', 'js', 'kml',
'm4a', 'm4v', 'mid', 'midi', 'mkv', 'mov', 'mp3', 'mp4', 'mpa', 'mpeg', 'mpg', 'ogg', 'ogv', 'pages',
'pcx', 'pdf', 'png', 'pps', 'ppt', 'pptx', 'potx', 'potm', 'ra', 'ram', 'rm', 'rtf', 'sit', 'sitx',
'tar', 'tgz', 'tif', 'tiff', 'txt', 'wav', 'webm', 'wma', 'wmv', 'xls', 'xlsx', 'xltx', 'xltm', 'zip',
'zipx',
);
/**
* @config
* @var array Category identifiers mapped to commonly used extensions.
*/
private static $app_categories = array(
'archive' => array(
'ace', 'arc', 'arj', 'bz', 'bz2', 'cab', 'dmg', 'gz', 'hqx', 'jar', 'rar', 'sit', 'sitx', 'tar', 'tgz',
'zip', 'zipx',
),
'audio' => array(
'aif', 'aifc', 'aiff', 'apl', 'au', 'avr', 'cda', 'm4a', 'mid', 'midi', 'mp3', 'ogg', 'ra',
'ram', 'rm', 'snd', 'wav', 'wma',
),
'document' => array(
'css', 'csv', 'doc', 'docx', 'dotm', 'dotx', 'htm', 'html', 'gpx', 'js', 'kml', 'pages', 'pdf',
'potm', 'potx', 'pps', 'ppt', 'pptx', 'rtf', 'txt', 'xhtml', 'xls', 'xlsx', 'xltm', 'xltx', 'xml',
),
'image' => array(
'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff',
),
'image/supported' => array(
'gif', 'jpeg', 'jpg', 'png'
),
'flash' => array(
'fla', 'swf'
),
'video' => array(
'asf', 'avi', 'flv', 'ifo', 'm1v', 'm2v', 'm4v', 'mkv', 'mov', 'mp2', 'mp4', 'mpa', 'mpe', 'mpeg',
'mpg', 'ogv', 'qt', 'vob', 'webm', 'wmv',
),
);
/**
* Map of file extensions to class type
*
* @config
* @var
*/
private static $class_for_file_extension = array(
'*' => 'SilverStripe\\Assets\\File',
'jpg' => 'SilverStripe\\Assets\\Image',
'jpeg' => 'SilverStripe\\Assets\\Image',
'png' => 'SilverStripe\\Assets\\Image',
'gif' => 'SilverStripe\\Assets\\Image',
);
/**
* @config
* @var bool If this is true, then restrictions set in {@link $allowed_max_file_size} and
* {@link $allowed_extensions} will be applied to users with admin privileges as
* well.
*/
private static $apply_restrictions_to_admin = true;
/**
* If enabled, legacy file dataobjects will be automatically imported into the APL
*
* @config
* @var bool
*/
private static $migrate_legacy_file = false;
/**
* @config
* @var boolean
*/
private static $update_filesystem = true;
public static function get_shortcodes()
{
return 'file_link';
}
/**
* Replace "[file_link id=n]" shortcode with an anchor tag or link to the file.
*
* @param array $arguments Arguments passed to the parser
* @param string $content Raw shortcode
* @param ShortcodeParser $parser Parser
* @param string $shortcode Name of shortcode used to register this handler
* @param array $extra Extra arguments
* @return string Result of the handled shortcode
*/
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array())
{
// Find appropriate record, with fallback for error handlers
$record = static::find_shortcode_record($arguments, $errorCode);
if ($errorCode) {
$record = static::find_error_record($errorCode);
}
if (!$record) {
return null; // There were no suitable matches at all.
}
// build the HTML tag
if ($content) {
// build some useful meta-data (file type and size) as data attributes
$attrs = ' ';
if ($record instanceof File) {
foreach (array(
'class' => 'file',
'data-type' => $record->getExtension(),
'data-size' => $record->getSize()
) as $name => $value) {
$attrs .= sprintf('%s="%s" ', $name, $value);
}
}
return sprintf('<a href="%s"%s>%s</a>', $record->Link(), rtrim($attrs), $parser->parse($content));
} else {
return $record->Link();
}
}
/**
* Find the record to use for a given shortcode.
*
* @param array $args Array of input shortcode arguments
* @param int $errorCode If the file is not found, or is inaccessible, this will be assigned to a HTTP error code.
* @return File|null The File DataObject, if it can be found.
*/
public static function find_shortcode_record($args, &$errorCode = null)
{
// Validate shortcode
if (!isset($args['id']) || !is_numeric($args['id'])) {
return null;
}
// Check if the file is found
/** @var File $file */
$file = File::get()->byID($args['id']);
if (!$file) {
$errorCode = 404;
return null;
}
// Check if the file is viewable
if (!$file->canView()) {
$errorCode = 403;
return null;
}
// Success
return $file;
}
/**
* Given a HTTP Error, find an appropriate substitute File or SiteTree data object instance.
*
* @param int $errorCode HTTP Error value
* @return File|SiteTree File or SiteTree object to use for the given error
*/
protected static function find_error_record($errorCode)
{
$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
$result = array_filter($result);
if ($result) {
return reset($result);
}
return null;
}
/**
* A file only exists if the file_exists() and is in the DB as a record
*
* Use $file->isInDB() to only check for a DB record
* Use $file->File->exists() to only check if the asset exists
*
* @return bool
*/
public function exists()
{
return parent::exists() && $this->File->exists();
}
/**
* Find a File object by the given filename.
*
* @param string $filename Filename to search for, including any custom parent directories.
* @return File
*/
public static function find($filename)
{
// Split to folders and the actual filename, and traverse the structure.
$parts = explode("/", $filename);
$parentID = 0;
/** @var File $item */
$item = null;
foreach ($parts as $part) {
$item = File::get()->filter(array(
'Name' => $part,
'ParentID' => $parentID
))->first();
if (!$item) {
break;
}
$parentID = $item->ID;
}
return $item;
}
/**
* Just an alias function to keep a consistent API with SiteTree
*
* @return string The link to the file
*/
public function Link()
{
return $this->getURL();
}
/**
* @deprecated 4.0
*/
public function RelativeLink()
{
Deprecation::notice('4.0', 'Use getURL instead, as not all files will be relative to the site root.');
return Director::makeRelative($this->getURL());
}
/**
* Just an alias function to keep a consistent API with SiteTree
*
* @return string The absolute link to the file
*/
public function AbsoluteLink()
{
return $this->getAbsoluteURL();
}
/**
* @return string
*/
public function getTreeTitle()
{
return Convert::raw2xml($this->Title);
}
/**
* @param Member $member
* @return bool
*/
public function canView($member = null)
{
if (!$member) {
$member = Member::currentUser();
}
$result = $this->extendedCan('canView', $member);
if ($result !== null) {
return $result;
}
return true;
}
/**
* Check if this file can be modified
*
* @param Member $member
* @return boolean
*/
public function canEdit($member = null)
{
if (!$member) {
$member = Member::currentUser();
}
$result = $this->extendedCan('canEdit', $member);
if ($result !== null) {
return $result;
}
return Permission::checkMember($member, array('CMS_ACCESS_AssetAdmin', 'CMS_ACCESS_LeftAndMain'));
}
/**
* Check if a file can be created
*
* @param Member $member
* @param array $context
* @return boolean
*/
public function canCreate($member = null, $context = array())
{
if (!$member) {
$member = Member::currentUser();
}
$result = $this->extendedCan('canCreate', $member, $context);
if ($result !== null) {
return $result;
}
return $this->canEdit($member);
}
/**
* Check if this file can be deleted
*
* @param Member $member
* @return boolean
*/
public function canDelete($member = null)
{
if (!$member) {
$member = Member::currentUser();
}
$result = $this->extendedCan('canDelete', $member);
if ($result !== null) {
return $result;
}
return $this->canEdit($member);
}
/**
* Returns the fields to power the edit screen of files in the CMS.
* You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
* and implementing updateCMSFields(FieldList $fields) on that extension.
*
* @return FieldList
*/
public function getCMSFields()
{
$path = '/' . dirname($this->getFilename());
$previewLink = Convert::raw2att($this->PreviewLink());
$image = "<img src=\"{$previewLink}\" class=\"editor__thumbnail\" />";
$statusTitle = $this->getStatusTitle();
$statusFlag = ($statusTitle) ? "<span class=\"editor__status-flag\">{$statusTitle}</span>" : '';
$content = Tab::create(
'Main',
HeaderField::create('TitleHeader', $this->Title, 1)
->addExtraClass('editor__heading'),
LiteralField::create('StatusFlag', $statusFlag),
LiteralField::create("IconFull", $image)
->addExtraClass('editor__file-preview'),
TabSet::create(
'Editor',
Tab::create(
'Details',
TextField::create("Title", $this->fieldLabel('Title')),
TextField::create("Name", $this->fieldLabel('Filename')),
ReadonlyField::create(
"Path",
_t('AssetTableField.PATH', 'Path'),
(($path !== '/.') ? $path : '') . '/'
)
),
Tab::create(
'Usage',
DatetimeField::create(
"Created",
_t('AssetTableField.CREATED', 'First uploaded')
)->setReadonly(true),
DatetimeField::create(
"LastEdited",
_t('AssetTableField.LASTEDIT', 'Last changed')
)->setReadonly(true)
)
),
HiddenField::create('ID', $this->ID)
);
$fields = FieldList::create(TabSet::create('Root', $content));
$this->extend('updateCMSFields', $fields);
return $fields;
}
/**
* Get title for current file status
*
* @return string
*/
public function getStatusTitle()
{
$statusTitle = '';
if ($this->isOnDraftOnly()) {
$statusTitle = _t('File.DRAFT', 'Draft');
} elseif ($this->isModifiedOnDraft()) {
$statusTitle = _t('File.MODIFIED', 'Modified');
}
return $statusTitle;
}
/**
* Returns a category based on the file extension.
* This can be useful when grouping files by type,
* showing icons on filelinks, etc.
* Possible group values are: "audio", "mov", "zip", "image".
*
* @param string $ext Extension to check
* @return string
*/
public static function get_app_category($ext)
{
$ext = strtolower($ext);
foreach (static::config()->app_categories as $category => $exts) {
if (in_array($ext, $exts)) {
return $category;
}
}
return false;
}
/**
* For a category or list of categories, get the list of file extensions
*
* @param array|string $categories List of categories, or single category
* @return array
*/
public static function get_category_extensions($categories)
{
if (empty($categories)) {
return array();
}
// Fix arguments into a single array
if (!is_array($categories)) {
$categories = array($categories);
} elseif (count($categories) === 1 && is_array(reset($categories))) {
$categories = reset($categories);
}
// Check configured categories
$appCategories = self::config()->app_categories;
// Merge all categories into list of extensions
$extensions = array();
foreach (array_filter($categories) as $category) {
if (isset($appCategories[$category])) {
$extensions = array_merge($extensions, $appCategories[$category]);
} else {
throw new InvalidArgumentException("Unknown file category: $category");
}
}
$extensions = array_unique($extensions);
sort($extensions);
return $extensions;
}
/**
* Returns a category based on the file extension.
*
* @return string
*/
public function appCategory()
{
return self::get_app_category($this->getExtension());
}
/**
* Should be called after the file was uploaded
*/
public function onAfterUpload()
{
$this->extend('onAfterUpload');
}
/**
* Make sure the file has a name
*/
protected function onBeforeWrite()
{
// Set default owner
if (!$this->isInDB() && !$this->OwnerID) {
$this->OwnerID = Member::currentUserID();
}
$name = $this->getField('Name');
$title = $this->getField('Title');
$changed = $this->isChanged('Name');
// Name can't be blank, default to Title
if (!$name) {
$changed = true;
$name = $title;
}
$filter = FileNameFilter::create();
if ($name) {
// Fix illegal characters
$name = $filter->filter($name);
} else {
// Default to file name
$changed = true;
$name = $this->i18n_singular_name();
$name = $filter->filter($name);
}
// Check for duplicates when the name has changed (or is set for the first time)
if ($changed) {
$nameGenerator = $this->getNameGenerator($name);
// Defaults to returning the original filename on first iteration
foreach ($nameGenerator as $newName) {
// This logic is also used in the Folder subclass, but we're querying
// for duplicates on the File base class here (including the Folder subclass).
// TODO Add read lock to avoid other processes creating files with the same name
// before this process has a chance to persist in the database.
$existingFile = File::get()->filter(array(
'Name' => $newName,
'ParentID' => (int) $this->ParentID
))->exclude(array(
'ID' => $this->ID
))->first();
if (!$existingFile) {
$name = $newName;
break;
}
}
}
// Update actual field value
$this->setField('Name', $name);
// Update title
if (!$title) {
// Generate a readable title, dashes and underscores replaced by whitespace,
// and any file extensions removed.
$this->setField(
'Title',
str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $name))
);
}
// Propagate changes to the AssetStore and update the DBFile field
$this->updateFilesystem();
parent::onBeforeWrite();
}
/**
* This will check if the parent record and/or name do not match the name on the underlying
* DBFile record, and if so, copy this file to the new location, and update the record to
* point to this new file.
*
* This method will update the File {@see DBFile} field value on success, so it must be called
* before writing to the database
*
* @return bool True if changed
*/
public function updateFilesystem()
{
if (!$this->config()->update_filesystem) {
return false;
}
// Check the file exists
if (!$this->File->exists()) {
return false;
}
// Avoid moving files on live; Rely on this being done on stage prior to publish.
if (Versioned::get_stage() !== Versioned::DRAFT) {
return false;
}
// Check path updated record will point to
// If no changes necessary, skip
$pathBefore = $this->File->getFilename();
$pathAfter = $this->generateFilename();
if ($pathAfter === $pathBefore) {
return false;
}
// Copy record to new location via stream
$stream = $this->File->getStream();
$this->File->setFromStream($stream, $pathAfter);
return true;
}
/**
* Collate selected descendants of this page.
* $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
* to the $collator array.
*
* @param string $condition The PHP condition to be evaluated. The page will be called $item
* @param array $collator An array, passed by reference, to collect all of the matching descendants.
* @return true|null
*/
public function collateDescendants($condition, &$collator)
{
if ($children = $this->Children()) {
foreach ($children as $item) {
/** @var File $item */
if (!$condition || eval("return $condition;")) {
$collator[] = $item;
}
$item->collateDescendants($condition, $collator);
}
return true;
}
return null;
}
/**
* Get an asset renamer for the given filename.
*
* @param string $filename Path name
* @return AssetNameGenerator
*/
protected function getNameGenerator($filename)
{
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
}
/**
* Gets the URL of this file
*
* @return string
*/
public function getAbsoluteURL()
{
$url = $this->getURL();
if ($url) {
return Director::absoluteURL($url);
}
return null;
}
/**
* Gets the URL of this file
*
* @uses Director::baseURL()
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* @return string
*/
public function getURL($grant = true)
{
if ($this->File->exists()) {
return $this->File->getURL($grant);
}
return null;
}
/**
* Get URL, but without resampling.
*
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* @return string
*/
public function getSourceURL($grant = true)
{
if ($this->File->exists()) {
return $this->File->getSourceURL($grant);
}
return null;
}
/**
* Get expected value of Filename tuple value. Will be used to trigger
* a file move on draft stage.
*
* @return string
*/
public function generateFilename()
{
// Check if this file is nested within a folder
$parent = $this->Parent();
if ($parent && $parent->exists()) {
return $this->join_paths($parent->getFilename(), $this->Name);
}
return $this->Name;
}
/**
* Ensure that parent folders are published before this one is published
*
* @todo Solve this via triggered publishing / ownership in the future
*/
public function onBeforePublish()
{
// Publish all parents from the root up
/** @var Folder $parent */
foreach ($this->getAncestors()->reverse() as $parent) {
$parent->publishSingle();
}
}
/**
* Update the ParentID and Name for the given filename.
*
* On save, the underlying DBFile record will move the underlying file to this location.
* Thus it will not update the underlying Filename value until this is done.
*
* @param string $filename
* @return $this
*/
public function setFilename($filename)
{
// Check existing folder path
$folder = '';
$parent = $this->Parent();
if ($parent && $parent->exists()) {
$folder = $parent->Filename;
}
// Detect change in foldername
$newFolder = ltrim(dirname(trim($filename, '/')), '.');
if ($folder !== $newFolder) {
if (!$newFolder) {
$this->ParentID = 0;
} else {
$parent = Folder::find_or_make($newFolder);
$this->ParentID = $parent->ID;
}
}
// Update base name
$this->Name = basename($filename);
return $this;
}
/**
* Returns the file extension
*
* @return string
*/
public function getExtension()
{
return self::get_file_extension($this->Name);
}
/**
* Gets the extension of a filepath or filename,
* by stripping away everything before the last "dot".
* Caution: Only returns the last extension in "double-barrelled"
* extensions (e.g. "gz" for "tar.gz").
*
* Examples:
* - "myfile" returns ""
* - "myfile.txt" returns "txt"
* - "myfile.tar.gz" returns "gz"
*
* @param string $filename
* @return string
*/
public static function get_file_extension($filename)
{
return pathinfo($filename, PATHINFO_EXTENSION);
}
/**
* Given an extension, determine the icon that should be used
*
* @param string $extension
* @return string Icon filename relative to base url
*/
public static function get_icon_for_extension($extension)
{
$extension = strtolower($extension);
// Check if exact extension has an icon
if (!file_exists(FRAMEWORK_PATH ."/client/images/app_icons/{$extension}_92.png")) {
$extension = static::get_app_category($extension);
// Fallback to category specific icon
if (!file_exists(FRAMEWORK_PATH ."/client/images/app_icons/{$extension}_92.png")) {
$extension ="generic";
}
}
return FRAMEWORK_DIR ."/client/images/app_icons/{$extension}_92.png";
}
/**
* Return the type of file for the given extension
* on the current file name.
*
* @return string
*/
public function getFileType()
{
return self::get_file_type($this->getFilename());
}
/**
* Get descriptive type of file based on filename
*
* @param string $filename
* @return string Description of file
*/
public static function get_file_type($filename)
{
$types = array(
'gif' => _t('File.GifType', 'GIF image - good for diagrams'),
'jpg' => _t('File.JpgType', 'JPEG image - good for photos'),
'jpeg' => _t('File.JpgType', 'JPEG image - good for photos'),
'png' => _t('File.PngType', 'PNG image - good general-purpose format'),
'ico' => _t('File.IcoType', 'Icon image'),
'tiff' => _t('File.TiffType', 'Tagged image format'),
'doc' => _t('File.DocType', 'Word document'),
'xls' => _t('File.XlsType', 'Excel spreadsheet'),
'zip' => _t('File.ZipType', 'ZIP compressed file'),
'gz' => _t('File.GzType', 'GZIP compressed file'),
'dmg' => _t('File.DmgType', 'Apple disk image'),
'pdf' => _t('File.PdfType', 'Adobe Acrobat PDF file'),
'mp3' => _t('File.Mp3Type', 'MP3 audio file'),
'wav' => _t('File.WavType', 'WAV audo file'),
'avi' => _t('File.AviType', 'AVI video file'),
'mpg' => _t('File.MpgType', 'MPEG video file'),
'mpeg' => _t('File.MpgType', 'MPEG video file'),
'js' => _t('File.JsType', 'Javascript file'),
'css' => _t('File.CssType', 'CSS file'),
'html' => _t('File.HtmlType', 'HTML file'),
'htm' => _t('File.HtmlType', 'HTML file')
);
// Get extension
$extension = strtolower(self::get_file_extension($filename));
return isset($types[$extension]) ? $types[$extension] : 'unknown';
}
/**
* Returns the size of the file type in an appropriate format.
*
* @return string|false String value, or false if doesn't exist
*/
public function getSize()
{
$size = $this->getAbsoluteSize();
if ($size) {
return static::format_size($size);
}
return false;
}
/**
* Formats a file size (eg: (int)42 becomes string '42 bytes')
*
* @param int $size
* @return string
*/
public static function format_size($size)
{
if ($size < 1024) {
return $size . ' bytes';
}
if ($size < 1024*10) {
return (round($size/1024*10)/10). ' KB';
}
if ($size < 1024*1024) {
return round($size/1024) . ' KB';
}
if ($size < 1024*1024*10) {
return (round(($size/1024)/1024*10)/10) . ' MB';
}
if ($size < 1024*1024*1024) {
return round(($size/1024)/1024) . ' MB';
}
return round($size/(1024*1024*1024)*10)/10 . ' GB';
}
/**
* Convert a php.ini value (eg: 512M) to bytes
*
* @param string $iniValue
* @return int
*/
public static function ini2bytes($iniValue)
{
$iniValues = str_split(trim($iniValue));
$unit = strtolower(array_pop($iniValues));
$quantity = (int) implode($iniValues);
switch ($unit) {
case 'g':
$quantity *= 1024;
// deliberate no break
case 'm':
$quantity *= 1024;
// deliberate no break
case 'k':
$quantity *= 1024;
// deliberate no break
default:
// no-op: pre-existing behaviour
break;
}
return $quantity;
}
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize()
{
return $this->File->getAbsoluteSize();
}
public function validate()
{
$result = ValidationResult::create();
$this->File->validate($result, $this->Name);
$this->extend('validate', $result);
return $result;
}
/**
* Maps a {@link File} subclass to a specific extension.
* By default, files with common image extensions will be created
* as {@link Image} instead of {@link File} when using
* {@link Folder::constructChild}, {@link Folder::addUploadToFolder}),
* and the {@link Upload} class (either directly or through {@link FileField}).
* For manually instanciated files please use this mapping getter.
*
* Caution: Changes to mapping doesn't apply to existing file records in the database.
* Also doesn't hook into {@link Object::getCustomClass()}.
*
* @param String File extension, without dot prefix. Use an asterisk ('*')
* to specify a generic fallback if no mapping is found for an extension.
* @return String Classname for a subclass of {@link File}
*/
public static function get_class_for_file_extension($ext)
{
$map = array_change_key_case(self::config()->class_for_file_extension, CASE_LOWER);
return (array_key_exists(strtolower($ext), $map)) ? $map[strtolower($ext)] : $map['*'];
}
/**
* See {@link get_class_for_file_extension()}.
*
* @param String|array
* @param String
*/
public static function set_class_for_file_extension($exts, $class)
{
if (!is_array($exts)) {
$exts = array($exts);
}
foreach ($exts as $ext) {
if (!is_subclass_of($class, 'SilverStripe\\Assets\\File')) {
throw new InvalidArgumentException(
sprintf('Class "%s" (for extension "%s") is not a valid subclass of File', $class, $ext)
);
}
self::config()->class_for_file_extension = array($ext => $class);
}
}
public function getMetaData()
{
if (!$this->File->exists()) {
return null;
}
return $this->File->getMetaData();
}
public function getMimeType()
{
if (!$this->File->exists()) {
return null;
}
return $this->File->getMimeType();
}
public function getStream()
{
if (!$this->File->exists()) {
return null;
}
return $this->File->getStream();
}
public function getString()
{
if (!$this->File->exists()) {
return null;
}
return $this->File->getString();
}
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
{
$result = $this->File->setFromLocalFile($path, $filename, $hash, $variant, $config);
// Update File record to name of the uploaded asset
if ($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
{
$result = $this->File->setFromStream($stream, $filename, $hash, $variant, $config);
// Update File record to name of the uploaded asset
if ($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
{
$result = $this->File->setFromString($data, $filename, $hash, $variant, $config);
// Update File record to name of the uploaded asset
if ($result) {
$this->setFilename($result['Filename']);
}
return $result;
}
public function getIsImage()
{
return false;
}
public function getFilename()
{
return $this->File->Filename;
}
public function getHash()
{
return $this->File->Hash;
}
public function getVariant()
{
return $this->File->Variant;
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function forTemplate()
{
return $this->getTag() ?: '';
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function getTag()
{
$template = $this->File->getFrontendTemplate();
if (empty($template)) {
return '';
}
return (string)$this->renderWith($template);
}
public function requireDefaultRecords()
{
parent::requireDefaultRecords();
// Check if old file records should be migrated
if (!$this->config()->migrate_legacy_file) {
return;
}
$migrated = FileMigrationHelper::singleton()->run();
if ($migrated) {
DB::alteration_message("{$migrated} File DataObjects upgraded", "changed");
}
}
/**
* Joins one or more segments together to build a Filename identifier.
*
* Note that the result will not have a leading slash, and should not be used
* with local file paths.
*
* @param string $part,... Parts
* @return string
*/
public static function join_paths($part = null)
{
$args = func_get_args();
if (count($args) === 1 && is_array($args[0])) {
$args = $args[0];
}
$parts = array();
foreach ($args as $arg) {
$part = trim($arg, ' \\/');
if ($part) {
$parts[] = $part;
}
}
return implode('/', $parts);
}
public function deleteFile()
{
return $this->File->deleteFile();
}
public function getVisibility()
{
return $this->File->getVisibility();
}
public function publishFile()
{
$this->File->publishFile();
}
public function protectFile()
{
$this->File->protectFile();
}
public function grantFile()
{
$this->File->grantFile();
}
public function revokeFile()
{
$this->File->revokeFile();
}
public function canViewFile()
{
return $this->File->canViewFile();
}
public function CMSEditLink()
{
$link = null;
$this->extend('updateCMSEditLink', $link);
return $link;
}
public function PreviewLink($action = null)
{
// Since AbsoluteURL can whitelist protected assets,
// do permission check first
if (!$this->canView()) {
return null;
}
$link = $this->getIcon();
$this->extend('updatePreviewLink', $link, $action);
return $link;
}
}

View File

@ -1,278 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Object;
use InvalidArgumentException;
/**
* A utility class that finds any files matching a set of rules that are
* present within a directory tree.
*
* Each file finder instance can have several options set on it:
* - name_regex (string): A regular expression that file basenames must match.
* - dir_regexp (string): A regular expression that dir basenames must match
* - accept_callback (callback): A callback that is called to accept a file.
* If it returns false the item will be skipped. The callback is passed the
* basename, pathname and depth.
* - accept_dir_callback (callback): The same as accept_callback, but only
* called for directories.
* - accept_file_callback (callback): The same as accept_callback, but only
* called for files.
* - file_callback (callback): A callback that is called when a file i
* succesfully matched. It is passed the basename, pathname and depth.
* - dir_callback (callback): The same as file_callback, but called for
* directories.
* - ignore_files (array): An array of file names to skip.
* - ignore_dirs (array): An array of directory names to skip.
* - ignore_vcs (bool): Skip over commonly used VCS dirs (svn, git, hg, bzr).
* This is enabled by default. The names of VCS directories to skip over
* are defined in {@link SS_FileFInder::$vcs_dirs}.
* - max_depth (int): The maxmium depth to traverse down the folder tree,
* default to unlimited.
*/
class FileFinder
{
/**
* @var array
*/
protected static $vcs_dirs = array(
'.git', '.svn', '.hg', '.bzr', 'node_modules',
);
/**
* The default options that are set on a new finder instance. Options not
* present in this array cannot be set.
*
* Any default_option statics defined on child classes are also taken into
* account.
*
* @var array
*/
protected static $default_options = array(
'name_regex' => null,
'dir_regex' => null,
'accept_callback' => null,
'accept_dir_callback' => null,
'accept_file_callback' => null,
'file_callback' => null,
'dir_callback' => null,
'ignore_files' => null,
'ignore_dirs' => null,
'ignore_vcs' => true,
'min_depth' => null,
'max_depth' => null
);
/**
* @var array
*/
protected $options;
public function __construct()
{
$this->options = array();
$class = get_class($this);
// We build our options array ourselves, because possibly no class or config manifest exists at this point
do {
$this->options = array_merge(Object::static_lookup($class, 'default_options'), $this->options);
} while ($class = get_parent_class($class));
}
/**
* Returns an option value set on this instance.
*
* @param string $name
* @return mixed
*/
public function getOption($name)
{
if (!array_key_exists($name, $this->options)) {
throw new InvalidArgumentException("The option $name doesn't exist.");
}
return $this->options[$name];
}
/**
* Set an option on this finder instance. See {@link SS_FileFinder} for the
* list of options available.
*
* @param string $name
* @param mixed $value
*/
public function setOption($name, $value)
{
if (!array_key_exists($name, $this->options)) {
throw new InvalidArgumentException("The option $name doesn't exist.");
}
$this->options[$name] = $value;
}
/**
* Sets several options at once.
*
* @param array $options
*/
public function setOptions(array $options)
{
foreach ($options as $k => $v) {
$this->setOption($k, $v);
}
}
/**
* Finds all files matching the options within a directory. The search is
* performed depth first.
*
* @param string $base
* @return array
*/
public function find($base)
{
$paths = array(array(rtrim($base, '/'), 0));
$found = array();
$fileCallback = $this->getOption('file_callback');
$dirCallback = $this->getOption('dir_callback');
while ($path = array_shift($paths)) {
list($path, $depth) = $path;
foreach (scandir($path) as $basename) {
if ($basename == '.' || $basename == '..') {
continue;
}
if (is_dir("$path/$basename")) {
if (!$this->acceptDir($basename, "$path/$basename", $depth + 1)) {
continue;
}
if ($dirCallback) {
call_user_func(
$dirCallback,
$basename,
"$path/$basename",
$depth + 1
);
}
$paths[] = array("$path/$basename", $depth + 1);
} else {
if (!$this->acceptFile($basename, "$path/$basename", $depth)) {
continue;
}
if ($fileCallback) {
call_user_func(
$fileCallback,
$basename,
"$path/$basename",
$depth
);
}
$found[] = "$path/$basename";
}
}
}
return $found;
}
/**
* Returns TRUE if the directory should be traversed. This can be overloaded
* to customise functionality, or extended with callbacks.
*
* @param string $basename
* @param string $pathname
* @param int $depth
* @return bool
*/
protected function acceptDir($basename, $pathname, $depth)
{
if ($regex = $this->getOption('dir_regex')) {
if (!preg_match($regex, $basename)) {
return false;
}
}
if ($this->getOption('ignore_vcs') && in_array($basename, self::$vcs_dirs)) {
return false;
}
if ($ignore = $this->getOption('ignore_dirs')) {
if (in_array($basename, $ignore)) {
return false;
}
}
if ($max = $this->getOption('max_depth')) {
if ($depth > $max) {
return false;
}
}
if ($callback = $this->getOption('accept_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) {
return false;
}
}
if ($callback = $this->getOption('accept_dir_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) {
return false;
}
}
return true;
}
/**
* Returns TRUE if the file should be included in the results. This can be
* overloaded to customise functionality, or extended via callbacks.
*
* @param string $basename
* @param string $pathname
* @param int $depth
* @return bool
*/
protected function acceptFile($basename, $pathname, $depth)
{
if ($regex = $this->getOption('name_regex')) {
if (!preg_match($regex, $basename)) {
return false;
}
}
if ($ignore = $this->getOption('ignore_files')) {
if (in_array($basename, $ignore)) {
return false;
}
}
if ($minDepth = $this->getOption('min_depth')) {
if ($depth < $minDepth) {
return false;
}
}
if ($callback = $this->getOption('accept_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) {
return false;
}
}
if ($callback = $this->getOption('accept_file_callback')) {
if (!call_user_func($callback, $basename, $pathname, $depth)) {
return false;
}
}
return true;
}
}

View File

@ -1,127 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\Object;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Service to help migrate File dataobjects to the new APL.
*
* This service does not alter these records in such a way that prevents downgrading back to 3.x
*/
class FileMigrationHelper extends Object
{
/**
* Perform migration
*
* @param string $base Absolute base path (parent of assets folder). Will default to BASE_PATH
* @return int Number of files successfully migrated
*/
public function run($base = null)
{
if (empty($base)) {
$base = BASE_PATH;
}
// Check if the File dataobject has a "Filename" field.
// If not, cannot migrate
/** @skipUpgrade */
if (!DB::get_schema()->hasField('File', 'Filename')) {
return 0;
}
// Set max time and memory limit
increase_time_limit_to();
increase_memory_limit_to();
// Loop over all files
$count = 0;
$originalState = Versioned::get_reading_mode();
Versioned::set_stage(Versioned::DRAFT);
$filenameMap = $this->getFilenameArray();
foreach ($this->getFileQuery() as $file) {
// Get the name of the file to import
$filename = $filenameMap[$file->ID];
$success = $this->migrateFile($base, $file, $filename);
if ($success) {
$count++;
}
}
Versioned::set_reading_mode($originalState);
return $count;
}
/**
* Migrate a single file
*
* @param string $base Absolute base path (parent of assets folder)
* @param File $file
* @param string $legacyFilename
* @return bool True if this file is imported successfully
*/
protected function migrateFile($base, File $file, $legacyFilename)
{
// Make sure this legacy file actually exists
$path = $base . '/' . $legacyFilename;
if (!file_exists($path)) {
return false;
}
// Copy local file into this filesystem
$filename = $file->generateFilename();
$result = $file->setFromLocalFile(
$path,
$filename,
null,
null,
array('conflict' => AssetStore::CONFLICT_OVERWRITE)
);
// Move file if the APL changes filename value
if ($result['Filename'] !== $filename) {
$file->setFilename($result['Filename']);
}
// Save and publish
$file->write();
$file->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
return true;
}
/**
* Get list of File dataobjects to import
*
* @return DataList
*/
protected function getFileQuery()
{
// Select all records which have a Filename value, but not FileFilename.
/** @skipUpgrade */
return File::get()
->exclude('ClassName', ['SilverStripe\\Assets\\Folder', 'Folder'])
->filter('FileFilename', array('', null))
->where('"File"."Filename" IS NOT NULL AND "File"."Filename" != \'\''); // Non-orm field
}
/**
* Get map of File IDs to legacy filenames
*
* @return array
*/
protected function getFilenameArray()
{
// Convert original query, ensuring the legacy "Filename" is included in the result
/** @skipUpgrade */
return $this
->getFileQuery()
->dataQuery()
->selectFromTable('File', array('ID', 'Filename'))
->execute()
->map(); // map ID to Filename
}
}

View File

@ -1,140 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Object;
use SilverStripe\View\Parsers\Transliterator;
/**
* Filter certain characters from file name, for nicer (more SEO-friendly) URLs
* as well as better filesystem compatibility. Can be used for files and folders.
*
* Caution: Does not take care of full filename sanitization in regards to directory traversal etc.,
* please use PHP's built-in basename() for this purpose.
*
* The default sanitizer is quite conservative regarding non-ASCII characters,
* in order to achieve maximum filesystem compatibility.
* In case your filesystem supports a wider character set,
* or is case sensitive, you might want to relax these rules
* via overriding {@link FileNameFilter_DefaultFilter::$default_replacements}.
*
* To leave uploaded filenames as they are (being aware of filesystem restrictions),
* add the following code to your YAML config:
* <code>
* FileNameFilter:
* default_use_transliterator: false
* default_replacements:
* </code>
*
* See {@link URLSegmentFilter} for a more generic implementation.
*/
class FileNameFilter extends Object
{
/**
* @config
* @var Boolean
*/
private static $default_use_transliterator = true;
/**
* @config
* @var array See {@link setReplacements()}.
*/
private static $default_replacements = array(
'/\s/' => '-', // remove whitespace
'/_/' => '-', // underscores to dashes
'/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
'/[\-]{2,}/' => '-', // remove duplicate dashes
'/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
);
/**
* @var array See {@link setReplacements()}
*/
public $replacements = array();
/**
* Depending on the applied replacement rules, this method
* might result in an empty string. In this case, {@link getDefaultName()}
* will be used to return a randomly generated file name, while retaining its extension.
*
* @param string $name including extension (not path).
* @return string A filtered filename
*/
public function filter($name)
{
$ext = pathinfo($name, PATHINFO_EXTENSION);
$transliterator = $this->getTransliterator();
if ($transliterator) {
$name = $transliterator->toASCII($name);
}
foreach ($this->getReplacements() as $regex => $replace) {
$name = preg_replace($regex, $replace, $name);
}
// Safeguard against empty file names
$nameWithoutExt = pathinfo($name, PATHINFO_FILENAME);
if (empty($nameWithoutExt)) {
$name = $this->getDefaultName();
$name .= $ext ? '.' . $ext : '';
}
return $name;
}
/**
* Take care not to add replacements which might invalidate the file structure,
* e.g. removing dots will remove file extension information.
*
* @param array $r Map of find/replace used for preg_replace().
*/
public function setReplacements($r)
{
$this->replacements = $r;
}
/**
* @return array
*/
public function getReplacements()
{
return ($this->replacements) ? $this->replacements : (array)$this->config()->default_replacements;
}
/**
* Transliterator instance, or false to disable.
* If null will use default.
*
* @var Transliterator|false
*/
protected $transliterator;
/**
* @return Transliterator
*/
public function getTransliterator()
{
if ($this->transliterator === null && $this->config()->default_use_transliterator) {
$this->transliterator = Transliterator::create();
}
return $this->transliterator;
}
/**
* @param Transliterator|false $t
*/
public function setTransliterator($t)
{
$this->transliterator = $t;
}
/**
* @return String File name without extension
*/
public function getDefaultName()
{
return (string)uniqid();
}
}

View File

@ -1,183 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Object;
use SilverStripe\Control\Director;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
/**
* A collection of static methods for manipulating the filesystem.
*/
class Filesystem extends Object
{
/**
* @config
* @var integer Integer
*/
private static $file_create_mask = 02775;
/**
* @config
* @var integer Integer
*/
private static $folder_create_mask = 02775;
/**
* @var int
*/
protected static $cache_folderModTime;
/**
* Create a folder on the filesystem, recursively.
* Uses {@link Filesystem::$folder_create_mask} to set filesystem permissions.
* Use {@link Folder::findOrMake()} to create a {@link Folder} database
* record automatically.
*
* @param String $folder Absolute folder path
*/
public static function makeFolder($folder)
{
if (!file_exists($base = dirname($folder))) {
self::makeFolder($base);
}
if (!file_exists($folder)) {
mkdir($folder, static::config()->folder_create_mask);
}
}
/**
* Remove a directory and all subdirectories and files.
*
* @param String $folder Absolute folder path
* @param Boolean $contentsOnly If this is true then the contents of the folder will be removed but not the
* folder itself
*/
public static function removeFolder($folder, $contentsOnly = false)
{
// remove a file encountered by a recursive call.
if (is_file($folder) || is_link($folder)) {
unlink($folder);
} else {
$dir = opendir($folder);
while ($file = readdir($dir)) {
if (($file == '.' || $file == '..')) {
continue;
} else {
self::removeFolder($folder . '/' . $file);
}
}
closedir($dir);
if (!$contentsOnly) {
rmdir($folder);
}
}
}
/**
* Remove a directory, but only if it is empty.
*
* @param string $folder Absolute folder path
* @param boolean $recursive Remove contained empty folders before attempting to remove this one
* @return boolean True on success, false on failure.
*/
public static function remove_folder_if_empty($folder, $recursive = true)
{
if (!is_readable($folder)) {
return false;
}
$handle = opendir($folder);
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
// if an empty folder is detected, remove that one first and move on
if ($recursive && is_dir($entry) && self::remove_folder_if_empty($entry)) {
continue;
}
// if a file was encountered, or a subdirectory was not empty, return false.
return false;
}
}
// if we are still here, the folder is empty.
rmdir($folder);
return true;
}
/**
* Cleanup function to reset all the Filename fields. Visit File/fixfiles to call.
*
* @deprecated 5.0
*/
public function fixfiles()
{
Deprecation::notice('5.0');
if (!Permission::check('ADMIN')) {
return Security::permissionFailure($this);
}
$files = File::get();
foreach ($files as $file) {
$file->updateFilesystem();
echo "<li>", $file->Filename;
$file->write();
}
echo "<p>Done!";
}
/**
* Return the most recent modification time of anything in the folder.
*
* @param string $folder The folder, relative to the site root
* @param array $extensionList An option array of file extensions to limit the search to
* @return string Same as filemtime() format.
*/
public static function folderModTime($folder, $extensionList = null)
{
$modTime = 0;
if (!Filesystem::isAbsolute($folder)) {
$folder = Director::baseFolder() . '/' . $folder;
}
$items = scandir($folder);
foreach ($items as $item) {
if ($item[0] != '.') {
// Recurse into folders
if (is_dir("$folder/$item")) {
$modTime = max($modTime, self::folderModTime("$folder/$item", $extensionList));
// Check files
} else {
$extension = null;
if ($extensionList) {
$extension = strtolower(substr($item, strrpos($item, '.')+1));
}
if (!$extensionList || in_array($extension, $extensionList)) {
$modTime = max($modTime, filemtime("$folder/$item"));
}
}
}
}
//if(!$recursiveCall) self::$cache_folderModTime[$cacheID] = $modTime;
return $modTime;
}
/**
* Returns true if the given filename is an absolute file reference.
* Works on Linux and Windows.
*
* @param String $filename Absolute or relative filename, with or without path.
* @return Boolean
*/
public static function isAbsolute($filename)
{
if ($_ENV['OS'] == "Windows_NT" || $_SERVER['WINDIR']) {
return $filename[1] == ':' && $filename[2] == '/';
} else {
return $filename[0] == '/';
}
}
}

View File

@ -1,153 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Config as FlysystemConfig;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\ORM\ArrayList;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
/**
* Adapter for local filesystem based on assets directory
*/
class AssetAdapter extends Local
{
use Configurable;
/**
* Server specific configuration necessary to block http traffic to a local folder
*
* @config
* @var array Mapping of server configurations to configuration files necessary
*/
private static $server_configuration = array();
/**
* Config compatible permissions configuration
*
* @config
* @var array
*/
private static $file_permissions = array(
'file' => [
'public' => 0744,
'private' => 0700,
],
'dir' => [
'public' => 0755,
'private' => 0700,
]
);
public function __construct($root = null, $writeFlags = LOCK_EX, $linkHandling = self::DISALLOW_LINKS)
{
// Get root path, and ensure that this exists and is safe
$root = $this->findRoot($root);
Filesystem::makeFolder($root);
$root = realpath($root);
// Override permissions with config
$permissions = $this->config()->get('file_permissions');
parent::__construct($root, $writeFlags, $linkHandling, $permissions);
// Configure server
$this->configureServer();
}
/**
* Determine the root folder absolute system path
*
* @param string $root
* @return string
*/
protected function findRoot($root)
{
// Empty root will set the path to assets
if (!$root) {
throw new \InvalidArgumentException("Missing argument for root path");
}
// Substitute leading ./ with BASE_PATH
if (strpos($root, './') === 0) {
return BASE_PATH . substr($root, 1);
}
// Substitute leading ./ with parent of BASE_PATH, in case storage is outside of the webroot.
if (strpos($root, '../') === 0) {
return dirname(BASE_PATH) . substr($root, 2);
}
return $root;
}
/**
* Force flush and regeneration of server files
*/
public function flush()
{
$this->configureServer(true);
}
/**
* Configure server files for this store
*
* @param bool $forceOverwrite Force regeneration even if files already exist
* @throws \Exception
*/
protected function configureServer($forceOverwrite = false)
{
// Get server type
$type = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '*';
list($type) = explode('/', strtolower($type));
// Determine configurations to write
$rules = $this->config()->get('server_configuration');
if (empty($rules[$type])) {
return;
}
$configurations = $rules[$type];
// Apply each configuration
$config = new FlysystemConfig();
$config->set('visibility', 'private');
foreach ($configurations as $file => $template) {
if ($forceOverwrite || !$this->has($file)) {
// Evaluate file
$content = $this->renderTemplate($template);
$success = $this->write($file, $content, $config);
if (!$success) {
throw new \Exception("Error writing server configuration file \"{$file}\"");
}
}
}
}
/**
* Render server configuration file from a template file
*
* @param string $template
* @return string Rendered results
*/
protected function renderTemplate($template)
{
// Build allowed extensions
$allowedExtensions = new ArrayList();
foreach (File::config()->allowed_extensions as $extension) {
if ($extension) {
$allowedExtensions->push(new ArrayData(array(
'Extension' => preg_quote($extension)
)));
}
}
$viewer = new SSViewer(array($template));
return (string)$viewer->process(new ArrayData(array(
'AllowedExtensions' => $allowedExtensions
)));
}
}

View File

@ -1,910 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use Generator;
use LogicException;
use InvalidArgumentException;
use League\Flysystem\Directory;
use League\Flysystem\Exception;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\Util;
use SilverStripe\Assets\Storage\AssetNameGenerator;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\AssetStoreRouter;
use SilverStripe\Control\Director;
use SilverStripe\Control\Session;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Injector\Injector;
/**
* Asset store based on flysystem Filesystem as a backend
*/
class FlysystemAssetStore implements AssetStore, AssetStoreRouter, Flushable
{
use Configurable;
/**
* Session key to use for user grants
*/
const GRANTS_SESSION = 'AssetStore_Grants';
/**
* @var Filesystem
*/
private $publicFilesystem = null;
/**
* Filesystem to use for protected files
*
* @var Filesystem
*/
private $protectedFilesystem = null;
/**
* Enable to use legacy filename behaviour (omits hash)
*
* Note that if using legacy filenames then duplicate files will not work.
*
* @config
* @var bool
*/
private static $legacy_filenames = false;
/**
* Flag if empty folders are allowed.
* If false, empty folders are cleared up when their contents are deleted.
*
* @config
* @var bool
*/
private static $keep_empty_dirs = false;
/**
* Set HTTP error code for requests to secure denied assets.
* Note that this defaults to 404 to prevent information disclosure
* of secure files
*
* @config
* @var int
*/
private static $denied_response_code = 404;
/**
* Set HTTP error code to use for missing secure assets
*
* @config
* @var int
*/
private static $missing_response_code = 404;
/**
* Custom headers to add to all custom file responses
*
* @config
* @var array
*/
private static $file_response_headers = array(
'Cache-Control' => 'private'
);
/**
* Assign new flysystem backend
*
* @param Filesystem $filesystem
* @return $this
*/
public function setPublicFilesystem(Filesystem $filesystem)
{
if (!$filesystem->getAdapter() instanceof PublicAdapter) {
throw new InvalidArgumentException("Configured adapter must implement PublicAdapter");
}
$this->publicFilesystem = $filesystem;
return $this;
}
/**
* Get the currently assigned flysystem backend
*
* @return Filesystem
* @throws LogicException
*/
public function getPublicFilesystem()
{
if (!$this->publicFilesystem) {
throw new LogicException("Filesystem misconfiguration error");
}
return $this->publicFilesystem;
}
/**
* Assign filesystem to use for non-public files
*
* @param Filesystem $filesystem
* @return $this
*/
public function setProtectedFilesystem(Filesystem $filesystem)
{
if (!$filesystem->getAdapter() instanceof ProtectedAdapter) {
throw new InvalidArgumentException("Configured adapter must implement ProtectedAdapter");
}
$this->protectedFilesystem = $filesystem;
return $this;
}
/**
* Get filesystem to use for non-public files
*
* @return Filesystem
* @throws Exception
*/
public function getProtectedFilesystem()
{
if (!$this->protectedFilesystem) {
throw new Exception("Filesystem misconfiguration error");
}
return $this->protectedFilesystem;
}
/**
* Return the store that contains the given fileID
*
* @param string $fileID Internal file identifier
* @return Filesystem
*/
protected function getFilesystemFor($fileID)
{
if ($this->getPublicFilesystem()->has($fileID)) {
return $this->getPublicFilesystem();
}
if ($this->getProtectedFilesystem()->has($fileID)) {
return $this->getProtectedFilesystem();
}
return null;
}
public function getCapabilities()
{
return array(
'visibility' => array(
self::VISIBILITY_PUBLIC,
self::VISIBILITY_PROTECTED
),
'conflict' => array(
self::CONFLICT_EXCEPTION,
self::CONFLICT_OVERWRITE,
self::CONFLICT_RENAME,
self::CONFLICT_USE_EXISTING
)
);
}
public function getVisibility($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
if ($this->getPublicFilesystem()->has($fileID)) {
return self::VISIBILITY_PUBLIC;
}
if ($this->getProtectedFilesystem()->has($fileID)) {
return self::VISIBILITY_PROTECTED;
}
return null;
}
public function getAsStream($filename, $hash, $variant = null)
{
$fileID = $this->getFileID($filename, $hash, $variant);
return $this
->getFilesystemFor($fileID)
->readStream($fileID);
}
public function getAsString($filename, $hash, $variant = null)
{
$fileID = $this->getFileID($filename, $hash, $variant);
return $this
->getFilesystemFor($fileID)
->read($fileID);
}
public function getAsURL($filename, $hash, $variant = null, $grant = true)
{
if ($grant) {
$this->grant($filename, $hash);
}
$fileID = $this->getFileID($filename, $hash, $variant);
// Check with filesystem this asset exists in
$public = $this->getPublicFilesystem();
$protected = $this->getProtectedFilesystem();
if ($public->has($fileID) || !$protected->has($fileID)) {
/** @var PublicAdapter $publicAdapter */
$publicAdapter = $public->getAdapter();
return $publicAdapter->getPublicUrl($fileID);
} else {
/** @var ProtectedAdapter $protectedAdapter */
$protectedAdapter = $protected->getAdapter();
return $protectedAdapter->getProtectedUrl($fileID);
}
}
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
{
// Validate this file exists
if (!file_exists($path)) {
throw new InvalidArgumentException("$path does not exist");
}
// Get filename to save to
if (empty($filename)) {
$filename = basename($path);
}
// Callback for saving content
$callback = function (Filesystem $filesystem, $fileID) use ($path) {
// Read contents as string into flysystem
$handle = fopen($path, 'r');
if ($handle === false) {
throw new InvalidArgumentException("$path could not be opened for reading");
}
$result = $filesystem->putStream($fileID, $handle);
fclose($handle);
return $result;
};
// When saving original filename, generate hash
if (!$variant) {
$hash = sha1_file($path);
}
// Submit to conflict check
return $this->writeWithCallback($callback, $filename, $hash, $variant, $config);
}
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
{
// Callback for saving content
$callback = function (Filesystem $filesystem, $fileID) use ($data) {
return $filesystem->put($fileID, $data);
};
// When saving original filename, generate hash
if (!$variant) {
$hash = sha1($data);
}
// Submit to conflict check
return $this->writeWithCallback($callback, $filename, $hash, $variant, $config);
}
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
{
// If the stream isn't rewindable, write to a temporary filename
if (!$this->isSeekableStream($stream)) {
$path = $this->getStreamAsFile($stream);
$result = $this->setFromLocalFile($path, $filename, $hash, $variant, $config);
unlink($path);
return $result;
}
// Callback for saving content
$callback = function (Filesystem $filesystem, $fileID) use ($stream) {
return $filesystem->putStream($fileID, $stream);
};
// When saving original filename, generate hash
if (!$variant) {
$hash = $this->getStreamSHA1($stream);
}
// Submit to conflict check
return $this->writeWithCallback($callback, $filename, $hash, $variant, $config);
}
public function delete($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
$protected = $this->deleteFromFilesystem($fileID, $this->getProtectedFilesystem());
$public = $this->deleteFromFilesystem($fileID, $this->getPublicFilesystem());
return $protected || $public;
}
/**
* Delete the given file (and any variants) in the given {@see Filesystem}
*
* @param string $fileID
* @param Filesystem $filesystem
* @return bool True if a file was deleted
*/
protected function deleteFromFilesystem($fileID, Filesystem $filesystem)
{
$deleted = false;
foreach ($this->findVariants($fileID, $filesystem) as $nextID) {
$filesystem->delete($nextID);
$deleted = true;
}
// Truncate empty dirs
$this->truncateDirectory(dirname($fileID), $filesystem);
return $deleted;
}
/**
* Clear directory if it's empty
*
* @param string $dirname Name of directory
* @param Filesystem $filesystem
*/
protected function truncateDirectory($dirname, Filesystem $filesystem)
{
if ($dirname
&& ltrim(dirname($dirname), '.')
&& ! $this->config()->get('keep_empty_dirs')
&& ! $filesystem->listContents($dirname)
) {
$filesystem->deleteDir($dirname);
}
}
/**
* Returns an iterable {@see Generator} of all files / variants for the given $fileID in the given $filesystem
* This includes the empty (no) variant.
*
* @param string $fileID ID of original file to compare with.
* @param Filesystem $filesystem
* @return Generator
*/
protected function findVariants($fileID, Filesystem $filesystem)
{
$dirname = ltrim(dirname($fileID), '.');
foreach ($filesystem->listContents($dirname) as $next) {
if ($next['type'] !== 'file') {
continue;
}
$nextID = $next['path'];
// Compare given file to target, omitting variant
if ($fileID === $this->removeVariant($nextID)) {
yield $nextID;
}
}
}
public function publish($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
$protected = $this->getProtectedFilesystem();
$public = $this->getPublicFilesystem();
$this->moveBetweenFilesystems($fileID, $protected, $public);
}
public function protect($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
$public = $this->getPublicFilesystem();
$protected = $this->getProtectedFilesystem();
$this->moveBetweenFilesystems($fileID, $public, $protected);
}
/**
* Move a file (and its associative variants) between filesystems
*
* @param string $fileID
* @param Filesystem $from
* @param Filesystem $to
*/
protected function moveBetweenFilesystems($fileID, Filesystem $from, Filesystem $to)
{
foreach ($this->findVariants($fileID, $from) as $nextID) {
// Copy via stream
$stream = $from->readStream($nextID);
$to->putStream($nextID, $stream);
fclose($stream);
$from->delete($nextID);
}
// Truncate empty dirs
$this->truncateDirectory(dirname($fileID), $from);
}
public function grant($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
$granted = Session::get(self::GRANTS_SESSION) ?: array();
$granted[$fileID] = true;
Session::set(self::GRANTS_SESSION, $granted);
}
public function revoke($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
$granted = Session::get(self::GRANTS_SESSION) ?: array();
unset($granted[$fileID]);
if ($granted) {
Session::set(self::GRANTS_SESSION, $granted);
} else {
Session::clear(self::GRANTS_SESSION);
}
}
public function canView($filename, $hash)
{
$fileID = $this->getFileID($filename, $hash);
if ($this->getProtectedFilesystem()->has($fileID)) {
return $this->isGranted($fileID);
}
return true;
}
/**
* Determine if a grant exists for the given FileID
*
* @param string $fileID
* @return bool
*/
protected function isGranted($fileID)
{
// Since permissions are applied to the non-variant only,
// map back to the original file before checking
$originalID = $this->removeVariant($fileID);
$granted = Session::get(self::GRANTS_SESSION) ?: array();
return !empty($granted[$originalID]);
}
/**
* get sha1 hash from stream
*
* @param resource $stream
* @return string str1 hash
*/
protected function getStreamSHA1($stream)
{
Util::rewindStream($stream);
$context = hash_init('sha1');
hash_update_stream($context, $stream);
return hash_final($context);
}
/**
* Get stream as a file
*
* @param resource $stream
* @return string Filename of resulting stream content
* @throws Exception
*/
protected function getStreamAsFile($stream)
{
// Get temporary file and name
$file = tempnam(sys_get_temp_dir(), 'ssflysystem');
$buffer = fopen($file, 'w');
if (!$buffer) {
throw new Exception("Could not create temporary file");
}
// Transfer from given stream
Util::rewindStream($stream);
stream_copy_to_stream($stream, $buffer);
if (! fclose($buffer)) {
throw new Exception("Could not write stream to temporary file");
}
return $file;
}
/**
* Determine if this stream is seekable
*
* @param resource $stream
* @return bool True if this stream is seekable
*/
protected function isSeekableStream($stream)
{
return Util::isSeekableStream($stream);
}
/**
* Invokes the conflict resolution scheme on the given content, and invokes a callback if
* the storage request is approved.
*
* @param callable $callback Will be invoked and passed a fileID if the file should be stored
* @param string $filename Name for the resulting file
* @param string $hash SHA1 of the original file content
* @param string $variant Variant to write
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant)
* @throws Exception
*/
protected function writeWithCallback($callback, $filename, $hash, $variant = null, $config = array())
{
// Set default conflict resolution
if (empty($config['conflict'])) {
$conflictResolution = $this->getDefaultConflictResolution($variant);
} else {
$conflictResolution = $config['conflict'];
}
// Validate parameters
if ($variant && $conflictResolution === AssetStore::CONFLICT_RENAME) {
// As variants must follow predictable naming rules, they should not be dynamically renamed
throw new InvalidArgumentException("Rename cannot be used when writing variants");
}
if (!$filename) {
throw new InvalidArgumentException("Filename is missing");
}
if (!$hash) {
throw new InvalidArgumentException("File hash is missing");
}
$filename = $this->cleanFilename($filename);
$fileID = $this->getFileID($filename, $hash, $variant);
// Check conflict resolution scheme
$resolvedID = $this->resolveConflicts($conflictResolution, $fileID);
if ($resolvedID !== false) {
// Check if source file already exists on the filesystem
$mainID = $this->getFileID($filename, $hash);
$filesystem = $this->getFilesystemFor($mainID);
// If writing a new file use the correct visibility
if (!$filesystem) {
// Default to public store unless requesting protected store
if (isset($config['visibility']) && $config['visibility'] === self::VISIBILITY_PROTECTED) {
$filesystem = $this->getProtectedFilesystem();
} else {
$filesystem = $this->getPublicFilesystem();
}
}
// Submit and validate result
$result = $callback($filesystem, $resolvedID);
if (!$result) {
throw new Exception("Could not save {$filename}");
}
// in case conflict resolution renamed the file, return the renamed
$filename = $this->getOriginalFilename($resolvedID);
} elseif (empty($variant)) {
// If deferring to the existing file, return the sha of the existing file,
// unless we are writing a variant (which has the same hash value as its original file)
$stream = $this
->getFilesystemFor($fileID)
->readStream($fileID);
$hash = $this->getStreamSHA1($stream);
}
return array(
'Filename' => $filename,
'Hash' => $hash,
'Variant' => $variant
);
}
/**
* Choose a default conflict resolution
*
* @param string $variant
* @return string
*/
protected function getDefaultConflictResolution($variant)
{
// If using new naming scheme (segment by hash) it's normally safe to overwrite files.
// Variants are also normally safe to overwrite, since lazy-generation is implemented at a higher level.
$legacy = $this->useLegacyFilenames();
if (!$legacy || $variant) {
return AssetStore::CONFLICT_OVERWRITE;
}
// Legacy behaviour is to rename
return AssetStore::CONFLICT_RENAME;
}
/**
* Determine if legacy filenames should be used. These do not have hash path parts.
*
* @return bool
*/
protected function useLegacyFilenames()
{
return $this->config()->get('legacy_filenames');
}
public function getMetadata($filename, $hash, $variant = null)
{
$fileID = $this->getFileID($filename, $hash, $variant);
$filesystem = $this->getFilesystemFor($fileID);
if ($filesystem) {
return $filesystem->getMetadata($fileID);
}
return null;
}
public function getMimeType($filename, $hash, $variant = null)
{
$fileID = $this->getFileID($filename, $hash, $variant);
$filesystem = $this->getFilesystemFor($fileID);
if ($filesystem) {
return $filesystem->getMimetype($fileID);
}
return null;
}
public function exists($filename, $hash, $variant = null)
{
$fileID = $this->getFileID($filename, $hash, $variant);
$filesystem = $this->getFilesystemFor($fileID);
return !empty($filesystem);
}
/**
* Determine the path that should be written to, given the conflict resolution scheme
*
* @param string $conflictResolution
* @param string $fileID
* @return string|false Safe filename to write to. If false, then don't write, and use existing file.
* @throws Exception
*/
protected function resolveConflicts($conflictResolution, $fileID)
{
// If overwrite is requested, simply put
if ($conflictResolution === AssetStore::CONFLICT_OVERWRITE) {
return $fileID;
}
// Otherwise, check if this exists
$exists = $this->getFilesystemFor($fileID);
if (!$exists) {
return $fileID;
}
// Flysystem defaults to use_existing
switch ($conflictResolution) {
// Throw tantrum
case static::CONFLICT_EXCEPTION: {
throw new InvalidArgumentException("File already exists at path {$fileID}");
}
// Rename
case static::CONFLICT_RENAME: {
foreach ($this->fileGeneratorFor($fileID) as $candidate) {
if (!$this->getFilesystemFor($candidate)) {
return $candidate;
}
}
throw new InvalidArgumentException("File could not be renamed with path {$fileID}");
}
// Use existing file
case static::CONFLICT_USE_EXISTING:
default: {
return false;
}
}
}
/**
* Get an asset renamer for the given filename.
*
* @param string $fileID Adapter specific identifier for this file/version
* @return AssetNameGenerator
*/
protected function fileGeneratorFor($fileID)
{
return Injector::inst()->createWithArgs('AssetNameGenerator', array($fileID));
}
/**
* Performs filename cleanup before sending it back.
*
* This name should not contain hash or variants.
*
* @param string $filename
* @return string
*/
protected function cleanFilename($filename)
{
// Since we use double underscore to delimit variants, eradicate them from filename
return preg_replace('/_{2,}/', '_', $filename);
}
/**
* Given a FileID, map this back to the original filename, trimming variant and hash
*
* @param string $fileID Adapter specific identifier for this file/version
* @return string Filename for this file, omitting hash and variant
*/
protected function getOriginalFilename($fileID)
{
// Remove variant
$originalID = $this->removeVariant($fileID);
// Remove hash (unless using legacy filenames, without hash)
if ($this->useLegacyFilenames()) {
return $originalID;
} else {
return preg_replace(
'/(?<hash>[a-zA-Z0-9]{10}\\/)(?<name>[^\\/]+)$/',
'$2',
$originalID
);
}
}
/**
* Remove variant from a fileID
*
* @param string $fileID
* @return string FileID without variant
*/
protected function removeVariant($fileID)
{
// Check variant
if (preg_match('/^(?<before>((?<!__).)+)__(?<variant>[^\\.]+)(?<after>.*)$/', $fileID, $matches)) {
return $matches['before'] . $matches['after'];
}
// There is no variant, so return original value
return $fileID;
}
/**
* Map file tuple (hash, name, variant) to a filename to be used by flysystem
*
* The resulting file will look something like my/directory/EA775CB4D4/filename__variant.jpg
*
* @param string $filename Name of file
* @param string $hash Hash of original file
* @param string $variant (if given)
* @return string Adapter specific identifier for this file/version
*/
protected function getFileID($filename, $hash, $variant = null)
{
// Since we use double underscore to delimit variants, eradicate them from filename
$filename = $this->cleanFilename($filename);
$name = basename($filename);
// Split extension
$extension = null;
if (($pos = strpos($name, '.')) !== false) {
$extension = substr($name, $pos);
$name = substr($name, 0, $pos);
}
// Unless in legacy mode, inject hash just prior to the filename
if ($this->useLegacyFilenames()) {
$fileID = $name;
} else {
$fileID = substr($hash, 0, 10) . '/' . $name;
}
// Add directory
$dirname = ltrim(dirname($filename), '.');
if ($dirname) {
$fileID = $dirname . '/' . $fileID;
}
// Add variant
if ($variant) {
$fileID .= '__' . $variant;
}
// Add extension
if ($extension) {
$fileID .= $extension;
}
return $fileID;
}
/**
* Ensure each adapter re-generates its own server configuration files
*/
public static function flush()
{
// Ensure that this instance is constructed on flush, thus forcing
// bootstrapping of necessary .htaccess / web.config files
$instance = singleton('AssetStore');
if ($instance instanceof FlysystemAssetStore) {
$publicAdapter = $instance->getPublicFilesystem()->getAdapter();
if ($publicAdapter instanceof AssetAdapter) {
$publicAdapter->flush();
}
$protectedAdapter = $instance->getProtectedFilesystem()->getAdapter();
if ($protectedAdapter instanceof AssetAdapter) {
$protectedAdapter->flush();
}
}
}
public function getResponseFor($asset)
{
// Check if file exists
$filesystem = $this->getFilesystemFor($asset);
if (!$filesystem) {
return $this->createMissingResponse();
}
// Block directory access
if ($filesystem->get($asset) instanceof Directory) {
return $this->createDeniedResponse();
}
// Deny if file is protected and denied
if ($filesystem === $this->getProtectedFilesystem() && !$this->isGranted($asset)) {
return $this->createDeniedResponse();
}
// Serve up file response
return $this->createResponseFor($filesystem, $asset);
}
/**
* Generate an {@see HTTPResponse} for the given file from the source filesystem
* @param FilesystemInterface $flysystem
* @param string $fileID
* @return HTTPResponse
*/
protected function createResponseFor(FilesystemInterface $flysystem, $fileID)
{
// Build response body
// @todo: gzip / buffer response?
$body = $flysystem->read($fileID);
$mime = $flysystem->getMimetype($fileID);
$response = new HTTPResponse($body, 200);
// Add headers
$response->addHeader('Content-Type', $mime);
$headers = $this->config()->get('file_response_headers');
foreach ($headers as $header => $value) {
$response->addHeader($header, $value);
}
return $response;
}
/**
* Generate a response for requests to a denied protected file
*
* @return HTTPResponse
*/
protected function createDeniedResponse()
{
$code = (int)$this->config()->get('denied_response_code');
return $this->createErrorResponse($code);
}
/**
* Generate a response for missing file requests
*
* @return HTTPResponse
*/
protected function createMissingResponse()
{
$code = (int)$this->config()->get('missing_response_code');
return $this->createErrorResponse($code);
}
/**
* Create a response with the given error code
*
* @param int $code
* @return HTTPResponse
*/
protected function createErrorResponse($code)
{
$response = new HTTPResponse('', $code);
// Show message in dev
if (!Director::isLive()) {
$response->setBody($response->getStatusDescription());
}
return $response;
}
}

View File

@ -1,116 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use Exception;
use League\Flysystem\Filesystem;
/**
* Simple Flysystem implementation of GeneratedAssetHandler for storing generated content
*/
class GeneratedAssetHandler implements \SilverStripe\Assets\Storage\GeneratedAssetHandler
{
/**
* Flysystem store for files
*
* @var Filesystem
*/
protected $assetStore = null;
/**
* Assign the asset backend. This must be a filesystem
* with an adapter of type {@see PublicAdapter}.
*
* @param Filesystem $store
* @return $this
*/
public function setFilesystem(Filesystem $store)
{
$this->assetStore = $store;
return $this;
}
/**
* Get the asset backend
*
* @return Filesystem
* @throws Exception
*/
public function getFilesystem()
{
if (!$this->assetStore) {
throw new Exception("Filesystem misconfiguration error");
}
return $this->assetStore;
}
public function getContentURL($filename, $callback = null)
{
$result = $this->checkOrCreate($filename, $callback);
if (!$result) {
return null;
}
/** @var PublicAdapter $adapter */
$adapter = $this
->getFilesystem()
->getAdapter();
return $adapter->getPublicUrl($filename);
}
public function getContent($filename, $callback = null)
{
$result = $this->checkOrCreate($filename, $callback);
if (!$result) {
return null;
}
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();
}
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use League\Flysystem\AdapterInterface;
/**
* An adapter which does not publicly expose protected files
*/
interface ProtectedAdapter extends AdapterInterface
{
/**
* Provide downloadable url that is restricted to granted users
*
* @param string $path
* @return string|null
*/
public function getProtectedUrl($path);
}

View File

@ -1,59 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use SilverStripe\Control\Director;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Config\Config;
class ProtectedAssetAdapter extends AssetAdapter implements ProtectedAdapter
{
/**
* Name of default folder to save secure assets in under ASSETS_PATH.
* This can be bypassed by specifying an absolute filesystem path via
* the SS_PROTECTED_ASSETS_PATH environment definition.
*
* @config
* @var string
*/
private static $secure_folder = '.protected';
private static $server_configuration = array(
'apache' => array(
'.htaccess' => "SilverStripe\\Assets\\Flysystem\\ProtectedAssetAdapter_HTAccess"
),
'microsoft-iis' => array(
'web.config' => "SilverStripe\\Assets\\Flysystem\\ProtectedAssetAdapter_WebConfig"
)
);
protected function findRoot($root)
{
// Use explicitly defined path
if ($root) {
return parent::findRoot($root);
}
// Use environment defined path or default location is under assets
if ($path = getenv('SS_PROTECTED_ASSETS_PATH')) {
return $path;
}
// Default location
return ASSETS_PATH . '/' . Config::inst()->get(__CLASS__, 'secure_folder');
}
/**
* Provide secure downloadable
*
* @param string $path
* @return string|null
*/
public function getProtectedUrl($path)
{
// Public URLs are handled via a request handler within /assets.
// If assets are stored locally, then asset paths of protected files should be equivalent.
return Controller::join_links(Director::baseURL(), ASSETS_DIR, $path);
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use League\Flysystem\AdapterInterface;
/**
* Represents an AbstractAdapter which exposes its assets via public urls
*/
interface PublicAdapter extends AdapterInterface
{
/**
* Provide downloadable url that is open to the public
*
* @param string $path
* @return string|null
*/
public function getPublicUrl($path);
}

View File

@ -1,60 +0,0 @@
<?php
namespace SilverStripe\Assets\Flysystem;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
class PublicAssetAdapter extends AssetAdapter implements PublicAdapter
{
/**
* Prefix between the root url and base of the assets folder
* Used for generating public urls
*
* @var string
*/
protected $parentUrlPrefix = null;
/**
* Server specific configuration necessary to block http traffic to a local folder
*
* @config
* @var array Mapping of server configurations to configuration files necessary
*/
private static $server_configuration = array(
'apache' => array(
'.htaccess' => "SilverStripe\\Assets\\Flysystem\\PublicAssetAdapter_HTAccess"
),
'microsoft-iis' => array(
'web.config' => "SilverStripe\\Assets\\Flysystem\\PublicAssetAdapter_WebConfig"
)
);
protected function findRoot($root)
{
if ($root) {
$path = parent::findRoot($root);
} else {
$path = ASSETS_PATH;
}
// Detect segment between root directory and assets root
if (stripos($path, BASE_PATH) === 0) {
$this->parentUrlPrefix = substr($path, strlen(BASE_PATH));
} else {
$this->parentUrlPrefix = ASSETS_DIR;
}
return $path;
}
/**
* Provide downloadable url
*
* @param string $path
* @return string|null
*/
public function getPublicUrl($path)
{
return Controller::join_links(Director::baseURL(), $this->parentUrlPrefix, $path);
}
}

View File

@ -1,355 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\TabSet;
/**
* Represents a logical folder, which may be used to organise assets
* stored in the configured backend.
*
* Unlike {@see File} dataobjects, there is not necessarily a physical filesystem entite which
* represents a Folder, and it may be purely logical. However, a physical folder may exist
* if the backend creates one.
*
* Additionally, folders do not have URLs (relative or absolute), nor do they have paths.
*
* When a folder is moved or renamed, records within it will automatically be copied to the updated
* location.
*
* Deleting a folder will remove all child records, but not any physical files.
*
* See {@link File} documentation for more details about the
* relationship between the database and filesystem in the SilverStripe file APIs.
*/
class Folder extends File
{
private static $singular_name = "Folder";
private static $plural_name = "Folders";
private static $table_name = 'Folder';
public function exists()
{
return $this->isInDB();
}
/**
* Find the given folder or create it as a database record
*
* @param string $folderPath Directory path relative to assets root
* @return Folder|null
*/
public static function find_or_make($folderPath)
{
// replace leading and trailing slashes
$folderPath = preg_replace('/^\/?(.*)\/?$/', '$1', trim($folderPath));
$parts = explode("/", $folderPath);
$parentID = 0;
$item = null;
$filter = FileNameFilter::create();
foreach ($parts as $part) {
if (!$part) {
continue; // happens for paths with a trailing slash
}
// Ensure search includes folders with illegal characters removed, but
// err in favour of matching existing folders if $folderPath
// includes illegal characters itself.
$partSafe = $filter->filter($part);
$item = Folder::get()->filter(array(
'ParentID' => $parentID,
'Name' => array($partSafe, $part)
))->first();
if (!$item) {
$item = new Folder();
$item->ParentID = $parentID;
$item->Name = $partSafe;
$item->Title = $part;
$item->write();
}
$parentID = $item->ID;
}
return $item;
}
public function onBeforeDelete()
{
foreach ($this->AllChildren() as $child) {
$child->delete();
}
parent::onBeforeDelete();
}
/**
* Return the relative URL of an icon for this file type
*
* @return string
*/
public function getIcon()
{
return FRAMEWORK_DIR . "/client/images/app_icons/folder_icon_large.png";
}
/**
* Override setting the Title of Folders to that Name and Title are always in sync.
* Note that this is not appropriate for files, because someone might want to create a human-readable name
* of a file that is different from its name on disk. But folders should always match their name on disk.
*
* @param string $title
* @return $this
*/
public function setTitle($title)
{
$this->setField('Title', $title);
$this->setField('Name', $title);
return $this;
}
/**
* Get the folder title
*
* @return string
*/
public function getTitle()
{
return $this->Name;
}
/**
* A folder doesn't have a (meaningful) file size.
*
* @return null
*/
public function getSize()
{
return null;
}
/**
* Returns all children of this folder
*
* @return DataList
*/
public function myChildren()
{
return File::get()->filter("ParentID", $this->ID);
}
/**
* Returns true if this folder has children
*
* @return bool
*/
public function hasChildren()
{
return $this->myChildren()->exists();
}
/**
* Returns true if this folder has children
*
* @return bool
*/
public function hasChildFolders()
{
return $this->ChildFolders()->exists();
}
/**
* Return the FieldList used to edit this folder in the CMS.
* You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
* and implemeting updateCMSFields(FieldList $fields) on that extension.
*
* @return FieldList
*/
public function getCMSFields()
{
// Don't show readonly path until we can implement parent folder selection,
// it's too confusing when readonly (makes sense for files only).
$width = (int)Image::config()->get('asset_preview_width');
$previewLink = Convert::raw2att($this->ScaleMaxWidth($width)->getIcon());
$image = "<img src=\"{$previewLink}\" class=\"editor__thumbnail\" />";
$content = Tab::create(
'Main',
HeaderField::create('TitleHeader', $this->Title, 1)
->addExtraClass('editor__heading'),
LiteralField::create("IconFull", $image)
->addExtraClass('editor__file-preview'),
TabSet::create(
'Editor',
Tab::create(
'Details',
TextField::create("Name", $this->fieldLabel('Filename'))
)
),
HiddenField::create('ID', $this->ID)
);
$fields = FieldList::create(TabSet::create('Root', $content));
$this->extend('updateCMSFields', $fields);
return $fields;
}
/**
* Get the children of this folder that are also folders.
*
* @return DataList
*/
public function ChildFolders()
{
return Folder::get()->filter('ParentID', $this->ID);
}
/**
* Get the number of children of this folder that are also folders.
*
* @return int
*/
public function numChildFolders()
{
return $this->ChildFolders()->count();
}
/**
* @return string
*/
public function CMSTreeClasses()
{
$classes = sprintf('class-%s', $this->class);
if (!$this->canDelete()) {
$classes .= " nodelete";
}
if (!$this->canEdit()) {
$classes .= " disabled";
}
$classes .= $this->markingClasses('numChildFolders');
return $classes;
}
/**
* @return string
*/
public function getTreeTitle()
{
return sprintf(
"<span class=\"jstree-foldericon\"></span><span class=\"item\">%s</span>",
Convert::raw2att(preg_replace('~\R~u', ' ', $this->Title))
);
}
public function getFilename()
{
return parent::generateFilename() . '/';
}
/**
* Folders do not have public URLs
*
* @param bool $grant
* @return null|string
*/
public function getURL($grant = true)
{
return null;
}
/**
* Folders do not have public URLs
*
* @return string
*/
public function getAbsoluteURL()
{
return null;
}
public function onAfterWrite()
{
parent::onAfterWrite();
// No publishing UX for folders, so just cascade changes live
if (Versioned::get_stage() === Versioned::DRAFT) {
$this->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
}
// Update draft version of all child records
$this->updateChildFilesystem();
}
public function onAfterDelete()
{
parent::onAfterDelete();
// Cascade deletions to live
if (Versioned::get_stage() === Versioned::DRAFT) {
$this->deleteFromStage(Versioned::LIVE);
}
}
public function updateFilesystem()
{
// No filesystem changes to update
}
/**
* If a write is skipped due to no changes, ensure that nested records still get asked to update
*/
public function onAfterSkippedWrite()
{
$this->updateChildFilesystem();
}
/**
* Update filesystem of all children
*/
public function updateChildFilesystem()
{
// Don't synchronise on live (rely on publishing instead)
if (Versioned::get_stage() === Versioned::LIVE) {
return;
}
$this->flushCache();
// Writing this record should trigger a write (and potential updateFilesystem) on each child
foreach ($this->AllChildren() as $child) {
$child->write();
}
}
public function StripThumbnail()
{
return null;
}
public function validate()
{
$result = ValidationResult::create();
$this->extend('validate', $result);
return $result;
}
}

View File

@ -1,767 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Storage\AssetStore;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Object;
use SilverStripe\Core\Flushable;
use InvalidArgumentException;
/**
* A wrapper class for GD-based images, with lots of manipulation functions.
*/
class GDBackend extends Object implements Image_Backend, Flushable
{
/**
* GD Resource
*
* @var resource
*/
protected $gd;
/**
* @var \Psr\SimpleCache\CacheInterface
*/
protected $cache;
/**
* @var int
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var int
*/
protected $quality;
/**
*
* @var int
*/
protected $interlace;
/**
* @config
* @var integer
*/
private static $default_quality = 75;
/**
* @config
* @var integer
*/
private static $image_interlace = 0;
public function __construct(AssetContainer $assetContainer = null)
{
parent::__construct();
$this->cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations');
if ($assetContainer) {
$this->loadFromContainer($assetContainer);
}
}
public function __destruct()
{
if ($resource = $this->getImageResource()) {
imagedestroy($resource);
}
}
public function loadFrom($path)
{
// If we're working with image resampling, things could take a while. Bump up the time-limit
increase_time_limit_to(300);
$this->resetResource();
// Skip if path is unavailable
if (!file_exists($path)) {
return;
}
$mtime = filemtime($path);
// Skip if load failed before
if ($this->failedResample($path, $mtime)) {
return;
}
// We use getimagesize instead of extension checking, because sometimes extensions are wrong.
$meta = getimagesize($path);
if ($meta === false || !$this->checkAvailableMemory($meta)) {
$this->markFailed($path, $mtime);
return;
}
$gd = null;
switch ($meta[2]) {
case 1:
if (function_exists('imagecreatefromgif')) {
$gd = imagecreatefromgif($path);
}
break;
case 2:
if (function_exists('imagecreatefromjpeg')) {
$gd = imagecreatefromjpeg($path);
}
break;
case 3:
if (function_exists('imagecreatefrompng')) {
$gd = imagecreatefrompng($path);
if ($gd) {
imagesavealpha($gd, true); // save alphablending setting (important)
}
}
break;
}
// image failed
if ($gd === false) {
$this->markFailed($path, $mtime);
return;
}
// Save
$this->setImageResource($gd);
}
public function loadFromContainer(AssetContainer $assetContainer)
{
// If we're working with image resampling, things could take a while. Bump up the time-limit
increase_time_limit_to(300);
$this->resetResource();
// Skip non-existant files
if (!$assetContainer->exists()) {
return;
}
// Skip if failed before, or image is too large
$filename = $assetContainer->getFilename();
$hash = $assetContainer->getHash();
$variant = $assetContainer->getVariant();
if ($this->failedResample($filename, $hash, $variant)) {
return;
}
$content = $assetContainer->getString();
// We use getimagesizefromstring instead of extension checking, because sometimes extensions are wrong.
$meta = getimagesizefromstring($content);
if ($meta === false || !$this->checkAvailableMemory($meta)) {
$this->markFailed($filename, $hash, $variant);
return;
}
// Mark as potentially failed prior to creation, resetting this on success
$image = imagecreatefromstring($content);
if ($image === false) {
$this->markFailed($filename, $hash, $variant);
return;
}
imagealphablending($image, false);
imagesavealpha($image, true); // save alphablending setting (important)
$this->setImageResource($image);
}
/**
* Clear GD resource
*/
protected function resetResource()
{
// Set defaults and clear resource
$this->setImageResource(null);
$this->quality = $this->config()->default_quality;
$this->interlace = $this->config()->image_interlace;
}
/**
* Assign or clear GD resource
*
* @param resource|null $resource
*/
public function setImageResource($resource)
{
$this->gd = $resource;
$this->width = $resource ? imagesx($resource) : 0;
$this->height = $resource ? imagesy($resource) : 0;
}
/**
* Get the currently assigned GD resource
*
* @return resource
*/
public function getImageResource()
{
return $this->gd;
}
/**
* Check if this image has previously crashed GD when attempting to open it - if it's opened
* successfully, the manipulation's cache key is removed.
*
* @param string $arg,... Any number of args that identify this image
* @return bool True if failed
*/
public function failedResample($arg = null)
{
$key = sha1(implode('|', func_get_args()));
return (bool)$this->cache->get($key);
}
/**
* Check if we've got enough memory available for resampling this image. This check is rough,
* so it will not catch all images that are too large - it also won't work accurately on large,
* animated GIFs as bits per pixel can't be calculated for an animated GIF with a global color
* table.
*
* @param array $imageInfo Value from getimagesize() or getimagesizefromstring()
* @return boolean
*/
protected function checkAvailableMemory($imageInfo)
{
$limit = translate_memstring(ini_get('memory_limit'));
if ($limit < 0) {
return true; // memory_limit == -1
}
// bits per channel (rounded up, default to 1)
$bits = isset($imageInfo['bits']) ? ($imageInfo['bits'] + 7) / 8 : 1;
// channels (default 4 rgba)
$channels = isset($imageInfo['channels']) ? $imageInfo['channels'] : 4;
$bytesPerPixel = $bits * $channels;
// width * height * bytes per pixel
$memoryRequired = $imageInfo[0] * $imageInfo[1] * $bytesPerPixel;
return $memoryRequired + memory_get_usage() < $limit;
}
/**
* Mark a file as failed
*
* @param string $arg,... Any number of args that identify this image
*/
protected function markFailed($arg = null)
{
$key = sha1(implode('|', func_get_args()));
$this->cache->set($key, '1');
}
/**
* Mark a file as succeeded
*
* @param string $arg,... Any number of args that identify this image
*/
protected function markSucceeded($arg = null)
{
$key = sha1(implode('|', func_get_args()));
$this->cache->set($key, '0');
}
public function setQuality($quality)
{
$this->quality = $quality;
}
public function croppedResize($width, $height)
{
if (!$this->gd) {
return null;
}
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if ($destAR < $srcAR) {
$srcY = 0;
$srcHeight = $this->height;
$srcWidth = round($this->height * $destAR);
$srcX = round(($this->width - $srcWidth) / 2);
// Destination shorter than the source
} else {
$srcX = 0;
$srcWidth = $this->width;
$srcHeight = round($this->width / $destAR);
$srcY = round(($this->height - $srcHeight) / 2);
}
imagecopyresampled($newGD, $this->gd, 0, 0, $srcX, $srcY, $width, $height, $srcWidth, $srcHeight);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Resizes the image to fit within the given region.
* Behaves similarly to paddedResize but without the padding.
* @todo This method isn't very efficent
*
* @param int $width
* @param int $height
* @return static
*/
public function fittedResize($width, $height)
{
$gd = $this->resizeByHeight($height);
if ($gd->width > $width) {
$gd = $gd->resizeByWidth($width);
}
return $gd;
}
/**
* @param int $width
* @param int $height
* @return static
*/
public function resize($width, $height)
{
if (!$this->gd) {
return null;
}
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
}
if (!$width && !$height) {
throw new InvalidArgumentException("No dimensions given when resizing image");
}
if (!$width) {
throw new InvalidArgumentException("Width not given when resizing image");
}
if (!$height) {
throw new InvalidArgumentException("Height not given when resizing image");
}
//use whole numbers, ensuring that size is at least 1x1
$width = max(1, round($width));
$height = max(1, round($height));
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle.
*
* @param float $angle Angle in degrees
* @return static
*/
public function rotate($angle)
{
if (!$this->gd) {
return null;
}
if (function_exists("imagerotate")) {
$newGD = imagerotate($this->gd, $angle, 0);
} else {
//imagerotate is not included in PHP included in Ubuntu
$newGD = $this->rotatePixelByPixel($angle);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Rotates image by given angle. It's slow because makes it pixel by pixel rather than
* using built-in function. Used when imagerotate function is not available(i.e. Ubuntu)
*
* @param float $angle Angle in degrees
* @return static
*/
public function rotatePixelByPixel($angle)
{
if (!$this->gd) {
return null;
}
$sourceWidth = imagesx($this->gd);
$sourceHeight = imagesy($this->gd);
if ($angle == 180) {
$destWidth = $sourceWidth;
$destHeight = $sourceHeight;
} else {
$destWidth = $sourceHeight;
$destHeight = $sourceWidth;
}
$rotate=imagecreatetruecolor($destWidth, $destHeight);
imagealphablending($rotate, false);
for ($x = 0; $x < ($sourceWidth); $x++) {
for ($y = 0; $y < ($sourceHeight); $y++) {
$color = imagecolorat($this->gd, $x, $y);
switch ($angle) {
case 90:
imagesetpixel($rotate, $y, $destHeight - $x - 1, $color);
break;
case 180:
imagesetpixel($rotate, $destWidth - $x - 1, $destHeight - $y - 1, $color);
break;
case 270:
imagesetpixel($rotate, $destWidth - $y - 1, $x, $color);
break;
default:
$rotate = $this->gd;
};
}
}
return $rotate;
}
/**
* Crop's part of image.
*
* @param int $top y position of left upper corner of crop rectangle
* @param int $left x position of left upper corner of crop rectangle
* @param int $width rectangle width
* @param int $height rectangle height
* @return static
*/
public function crop($top, $left, $width, $height)
{
if (!$this->gd) {
return null;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserve alpha channel between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
imagecopyresampled($newGD, $this->gd, 0, 0, $left, $top, $width, $height, $width, $height);
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Width of image.
*
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* Height of image.
*
* @return int
*/
public function getHeight()
{
return $this->height;
}
public function resizeByWidth($width)
{
$heightScale = $width / $this->width;
return $this->resize($width, $heightScale * $this->height);
}
/**
* @param int $height
* @return static
*/
public function resizeByHeight($height)
{
$scale = $height / $this->height;
return $this->resize($scale * $this->width, $height);
}
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false)
{
$widthRatio = $maxWidth / $this->width;
$heightRatio = $maxHeight / $this->height;
if ($widthRatio < $heightRatio) {
return $useAsMinimum
? $this->resizeByHeight($maxHeight)
: $this->resizeByWidth($maxWidth);
} else {
return $useAsMinimum
? $this->resizeByWidth($maxWidth)
: $this->resizeByHeight($maxHeight);
}
}
public function paddedResize($width, $height, $backgroundColor = "FFFFFF")
{
if (!$this->gd) {
return null;
}
$width = round($width);
$height = round($height);
// Check that a resize is actually necessary.
if ($width == $this->width && $height == $this->height) {
return $this;
}
$newGD = imagecreatetruecolor($width, $height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$bg = $this->colourWeb2GD($newGD, $backgroundColor);
imagefilledrectangle($newGD, 0, 0, $width, $height, $bg);
$destAR = $width / $height;
if ($this->width > 0 && $this->height > 0) {
// We can't divide by zero theres something wrong.
$srcAR = $this->width / $this->height;
// Destination narrower than the source
if ($destAR > $srcAR) {
$destY = 0;
$destHeight = $height;
$destWidth = round($height * $srcAR);
$destX = round(($width - $destWidth) / 2);
// Destination shorter than the source
} else {
$destX = 0;
$destWidth = $width;
$destHeight = round($width / $srcAR);
$destY = round(($height - $destHeight) / 2);
}
imagecopyresampled(
$newGD,
$this->gd,
$destX,
$destY,
0,
0,
$destWidth,
$destHeight,
$this->width,
$this->height
);
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
/**
* Make the image greyscale.
* Default color weights are based on standard BT.601 (those used in PAL, NTSC and many software packages, also see
* https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems )
*
* @param int $R red weight, defaults to 299
* @param int $G green weight, defaults to 587
* @param int $B blue weight, defaults to 114
* @param int $brightness brightness in percentage, defaults to 100
* @return GDBackend
*/
public function greyscale($R = 299, $G = 587, $B = 114, $brightness = 100)
{
if (!$this->gd) {
return null;
}
$width = $this->width;
$height = $this->height;
$newGD = imagecreatetruecolor($this->width, $this->height);
// Preserves transparency between images
imagealphablending($newGD, false);
imagesavealpha($newGD, true);
$rt = $R + $G + $B;
// if $rt is 0, bad parameters are provided, so result will be a black image
$rr = $rt ? $R/$rt : 0;
$gr = $rt ? $G/$rt : 0;
$br = $rt ? $B/$rt : 0;
// iterate over all pixels and make them grey
for ($dy = 0; $dy < $height; $dy++) {
for ($dx = 0; $dx < $width; $dx++) {
$pxrgb = imagecolorat($this->gd, $dx, $dy);
$heightgb = imagecolorsforindex($this->gd, $pxrgb);
$newcol = ($rr*$heightgb['red']) + ($br*$heightgb['blue']) + ($gr*$heightgb['green']);
$newcol = min(255, $newcol*$brightness/100);
$setcol = imagecolorallocatealpha($newGD, $newcol, $newcol, $newcol, $heightgb['alpha']);
imagesetpixel($newGD, $dx, $dy, $setcol);
}
}
$output = clone $this;
$output->setImageResource($newGD);
return $output;
}
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $config = array())
{
// Write to temporary file, taking care to maintain the extension
$path = tempnam(sys_get_temp_dir(), 'gd');
if ($extension = pathinfo($filename, PATHINFO_EXTENSION)) {
$path .= "." . $extension;
}
$writeSuccess = $this->writeTo($path);
if (!$writeSuccess) {
return null;
}
$result = $assetStore->setFromLocalFile($path, $filename, $hash, $variant, $config);
unlink($path);
return $result;
}
/**
* @param string $filename
* @return boolean
*/
public function writeTo($filename)
{
if (!$filename) {
return false;
}
// The GD resource might not exist if the image is too large to be processed, see checkAvailableMemory().
if (!$this->gd) {
return false;
}
// Get current image data
if (file_exists($filename)) {
list($width, $height, $type, $attr) = getimagesize($filename);
unlink($filename);
} else {
Filesystem::makeFolder(dirname($filename));
}
// If image type isn't known, guess from extension
$ext = strtolower(substr($filename, strrpos($filename, '.')+1));
if (empty($type)) {
switch ($ext) {
case "gif":
$type = IMAGETYPE_GIF;
break;
case "jpeg":
case "jpg":
case "jpe":
$type = IMAGETYPE_JPEG;
break;
default:
$type = IMAGETYPE_PNG;
break;
}
}
// If $this->interlace != 0, the output image will be interlaced.
imageinterlace($this->gd, $this->interlace);
// if the extension does not exist, the file will not be created!
switch ($type) {
case IMAGETYPE_GIF:
imagegif($this->gd, $filename);
break;
case IMAGETYPE_JPEG:
imagejpeg($this->gd, $filename, $this->quality);
break;
// case 3, and everything else
default:
// Save them as 8-bit images
// imagetruecolortopalette($this->gd, false, 256);
imagepng($this->gd, $filename);
break;
}
if (!file_exists($filename)) {
return false;
}
@chmod($filename, 0664);
return true;
}
/**
* Helper function to allocate a colour to an image
*
* @param resource $image
* @param string $webColor
* @return int
*/
protected function colourWeb2GD($image, $webColor)
{
if (substr($webColor, 0, 1) == "#") {
$webColor = substr($webColor, 1);
}
$r = hexdec(substr($webColor, 0, 2));
$g = hexdec(substr($webColor, 2, 2));
$b = hexdec(substr($webColor, 4, 2));
return imagecolorallocate($image, $r, $g, $b);
}
public static function flush()
{
$cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations');
$cache->clear();
}
}

View File

@ -1,255 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\HTMLReadonlyField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\View\Parsers\ShortcodeHandler;
use SilverStripe\Forms\Tab;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\DatetimeField;
use SilverStripe\Forms\ReadonlyField;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\FieldList;
/**
* Represents an Image
*/
class Image extends File implements ShortcodeHandler
{
/**
* @config
* @var string
*/
private static $table_name = 'Image';
/**
* @config
* @var string
*/
private static $singular_name = "Image";
/**
* @config
* @var string
*/
private static $plural_name = "Images";
public function __construct($record = null, $isSingleton = false, $model = null, $queryParams = array())
{
parent::__construct($record, $isSingleton, $model, $queryParams);
$this->File->setAllowedCategories('image/supported');
}
public function getCMSFields()
{
$path = '/' . dirname($this->getFilename());
$previewLink = Convert::raw2att($this->PreviewLink());
$image = "<img src=\"{$previewLink}\" class=\"editor__thumbnail\" />";
$link = $this->Link();
$statusTitle = $this->getStatusTitle();
$statusFlag = "<span class=\"editor__status-flag\">{$statusTitle}</span>";
$content = Tab::create(
'Main',
HeaderField::create('TitleHeader', $this->Title, 1)
->addExtraClass('editor__heading'),
LiteralField::create("ImageFull", $image)
->addExtraClass('editor__file-preview'),
TabSet::create(
'Editor',
Tab::create(
'Details',
TextField::create("Title", $this->fieldLabel('Title')),
TextField::create("Name", $this->fieldLabel('Filename')),
ReadonlyField::create(
"Path",
_t('AssetTableField.PATH', 'Path'),
(($path !== '/.') ? $path : '') . '/'
),
HTMLReadonlyField::create(
'ClickableURL',
_t('AssetTableField.URL', 'URL'),
sprintf(
'<i class="%s"></i><a href="%s" target="_blank">%s</a>',
'font-icon-link btn--icon-large form-control-static__icon',
$link,
$link
)
)
),
Tab::create(
'Usage',
DatetimeField::create(
"Created",
_t('AssetTableField.CREATED', 'First uploaded')
)->setReadonly(true),
DatetimeField::create(
"LastEdited",
_t('AssetTableField.LASTEDIT', 'Last changed')
)->setReadonly(true)
)
),
HiddenField::create('ID', $this->ID)
);
if ($dimensions = $this->getDimensions()) {
$content->insertAfter(
'TitleHeader',
LiteralField::create(
"DisplaySize",
sprintf(
'<div class="editor__specs">%spx, %s %s</div>',
$dimensions,
$this->getSize(),
$statusFlag
)
)
);
} else {
$content->insertAfter(
'TitleHeader',
LiteralField::create('StatusFlag', $statusFlag)
);
}
$fields = FieldList::create(TabSet::create('Root', $content));
$this->extend('updateCMSFields', $fields);
return $fields;
}
public function getIsImage()
{
return true;
}
/**
* Replace"[image id=n]" shortcode with an image reference.
* Permission checks will be enforced by the file routing itself.
*
* @param array $args Arguments passed to the parser
* @param string $content Raw shortcode
* @param ShortcodeParser $parser Parser
* @param string $shortcode Name of shortcode used to register this handler
* @param array $extra Extra arguments
* @return string Result of the handled shortcode
*/
public static function handle_shortcode($args, $content, $parser, $shortcode, $extra = array())
{
// Find appropriate record, with fallback for error handlers
$record = static::find_shortcode_record($args, $errorCode);
if ($errorCode) {
$record = static::find_error_record($errorCode);
}
if (!$record) {
return null; // There were no suitable matches at all.
}
// Check if a resize is required
$src = $record->Link();
if ($record instanceof Image) {
$width = isset($args['width']) ? $args['width'] : null;
$height = isset($args['height']) ? $args['height'] : null;
$hasCustomDimensions = ($width && $height);
if ($hasCustomDimensions && (($width != $record->getWidth()) || ($height != $record->getHeight()))) {
$resized = $record->ResizedImage($width, $height);
// Make sure that the resized image actually returns an image
if ($resized) {
$src = $resized->getURL();
}
}
}
// Build the HTML tag
$attrs = array_merge(
// Set overrideable defaults
['src' => '', 'alt' => $record->Title],
// Use all other shortcode arguments
$args,
// But enforce some values
['id' => '', 'src' => $src]
);
// Clean out any empty attributes
$attrs = array_filter($attrs, function ($v) {
return (bool)$v;
});
// Condense to HTML attribute string
$attrsStr = join(' ', array_map(function ($name) use ($attrs) {
return Convert::raw2att($name) . '="' . Convert::raw2att($attrs[$name]) . '"';
}, array_keys($attrs)));
return '<img ' . $attrsStr . ' />';
}
/**
* Regenerates "[image id=n]" shortcode with new src attribute prior to being edited within the CMS.
*
* @param array $args Arguments passed to the parser
* @param string $content Raw shortcode
* @param ShortcodeParser $parser Parser
* @param string $shortcode Name of shortcode used to register this handler
* @param array $extra Extra arguments
* @return string Result of the handled shortcode
*/
public static function regenerate_shortcode($args, $content, $parser, $shortcode, $extra = array())
{
// Check if there is a suitable record
$record = static::find_shortcode_record($args);
if ($record) {
$args['src'] = $record->getURL();
}
// Rebuild shortcode
$parts = array();
foreach ($args as $name => $value) {
$htmlValue = Convert::raw2att($value ?: $name);
$parts[] = sprintf('%s="%s"', $name, $htmlValue);
}
return sprintf("[%s %s]", $shortcode, implode(' ', $parts));
}
/**
* Helper method to regenerate all shortcode links.
*
* @param string $value HTML value
* @return string value with links resampled
*/
public static function regenerate_html_links($value)
{
// Create a shortcode generator which only regenerates links
$regenerator = ShortcodeParser::get('regenerator');
return $regenerator->parse($value);
}
public function PreviewLink($action = null)
{
// Since AbsoluteLink can whitelist protected assets,
// do permission check first
if (!$this->canView()) {
return false;
}
// Size to width / height
$width = (int)$this->config()->get('asset_preview_width');
$height = (int)$this->config()->get('asset_preview_height');
$resized = $this->FitMax($width, $height);
if ($resized && $resized->exists()) {
$link = $resized->getAbsoluteURL();
} else {
$link = $this->getIcon();
}
$this->extend('updatePreviewLink', $link, $action);
return $link;
}
}

View File

@ -1,828 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use InvalidArgumentException;
/**
* Provides image manipulation functionality.
* Provides limited thumbnail generation functionality for non-image files.
* Should only be applied to implementors of AssetContainer
*
* Allows raw images to be resampled via Resampled()
*
* Image scaling manipluations, including:
* - Fit()
* - FitMax()
* - ScaleWidth()
* - ScaleMaxWidth()
* - ScaleHeight()
* - ScaleMaxHeight()
* - ResizedImage()
*
* Image cropping manipulations, including:
* - CropHeight()
* - CropWidth()
* - Fill()
* - FillMax()
*
* Thumbnail generation methods including:
* - Icon()
* - CMSThumbnail()
*
* @mixin AssetContainer
*/
trait ImageManipulation
{
/**
* @return string Data from the file in this container
*/
abstract public function getString();
/**
* @return resource Data stream to the asset in this container
*/
abstract public function getStream();
/**
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* @return string public url to the asset in this container
*/
abstract public function getURL($grant = true);
/**
* @return string The absolute URL to the asset in this container
*/
abstract public function getAbsoluteURL();
/**
* Get metadata for this file
*
* @return array|null File information
*/
abstract public function getMetaData();
/**
* Get mime type
*
* @return string Mime type for this file
*/
abstract public function getMimeType();
/**
* Return file size in bytes.
*
* @return int
*/
abstract public function getAbsoluteSize();
/**
* Determine if this container has a valid value
*
* @return bool Flag as to whether the file exists
*/
abstract public function exists();
/**
* Get value of filename
*
* @return string
*/
abstract public function getFilename();
/**
* Get value of hash
*
* @return string
*/
abstract public function getHash();
/**
* Get value of variant
*
* @return string
*/
abstract public function getVariant();
/**
* Determine if a valid non-empty image exists behind this asset
*
* @return bool
*/
abstract public function getIsImage();
/**
* @config
* @var bool Force all images to resample in all cases
*/
private static $force_resample = true;
/**
* @config
* @var int The width of an image thumbnail in a strip.
*/
private static $strip_thumbnail_width = 50;
/**
* @config
* @var int The height of an image thumbnail in a strip.
*/
private static $strip_thumbnail_height = 50;
/**
* The width of an image thumbnail in the CMS.
*
* @config
* @var int
*/
private static $cms_thumbnail_width = 100;
/**
* The height of an image thumbnail in the CMS.
*
* @config
* @var int
*/
private static $cms_thumbnail_height = 100;
/**
* The width of an image preview in the Asset section
*
* @config
* @var int
*/
private static $asset_preview_width = 930; // max for mobile full-width
/**
* The height of an image preview in the Asset section
*
* @config
* @var int
*/
private static $asset_preview_height = 336;
/**
* Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad.
*
* @param integer $width The width to size to
* @param integer $height The height to size to
* @param string $backgroundColor
* @return AssetContainer
*/
public function Pad($width, $height, $backgroundColor = 'FFFFFF')
{
if ($this->isSize($width, $height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width, $height, $backgroundColor);
return $this->manipulateImage(
$variant,
function (Image_Backend $backend) use ($width, $height, $backgroundColor) {
return $backend->paddedResize($width, $height, $backgroundColor);
}
);
}
/**
* Forces the image to be resampled, if possible
*
* @return AssetContainer
*/
public function Resampled()
{
// If image is already resampled, return self reference
$variant = $this->getVariant();
if ($variant) {
return $this;
}
// Resample, but fallback to original object
$result = $this->manipulateImage(__FUNCTION__, function (Image_Backend $backend) {
return $backend;
});
if ($result) {
return $result;
}
return $this;
}
/**
* Update the url to point to a resampled version if forcing
*
* @param string $url
*/
public function updateURL(&$url)
{
// Skip if resampling is off, or is already resampled, or is not an image
if (!Config::inst()->get(get_class($this), 'force_resample') || $this->getVariant() || !$this->getIsImage()) {
return;
}
// Attempt to resample
$resampled = $this->Resampled();
if (!$resampled) {
return;
}
// Only update if resampled file is a smaller file size
if ($resampled->getAbsoluteSize() < $this->getAbsoluteSize()) {
$url = $resampled->getURL();
}
}
/**
* Generate a resized copy of this image with the given width & height.
* This can be used in templates with $ResizedImage but should be avoided,
* as it's the only image manipulation function which can skew an image.
*
* @param integer $width Width to resize to
* @param integer $height Height to resize to
* @return AssetContainer
*/
public function ResizedImage($width, $height)
{
if ($this->isSize($width, $height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function (Image_Backend $backend) use ($width, $height) {
return $backend->resize($width, $height);
});
}
/**
* Scale image proportionally to fit within the specified bounds
*
* @param integer $width The width to size within
* @param integer $height The height to size within
* @return AssetContainer
*/
public function Fit($width, $height)
{
// Prevent divide by zero on missing/blank file
if (!$this->getWidth() || !$this->getHeight()) {
return null;
}
// Check if image is already sized to the correct dimension
$widthRatio = $width / $this->getWidth();
$heightRatio = $height / $this->getHeight();
if ($widthRatio < $heightRatio) {
// Target is higher aspect ratio than image, so check width
if ($this->isWidth($width)) {
return $this;
}
} else {
// Target is wider or same aspect ratio as image, so check height
if ($this->isHeight($height)) {
return $this;
}
}
// Item must be regenerated
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function (Image_Backend $backend) use ($width, $height) {
return $backend->resizeRatio($width, $height);
});
}
/**
* Proportionally scale down this image if it is wider or taller than the specified dimensions.
* Similar to Fit but without up-sampling. Use in templates with $FitMax.
*
* @uses ScalingManipulation::Fit()
* @param integer $width The maximum width of the output image
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function FitMax($width, $height)
{
return $this->getWidth() > $width || $this->getHeight() > $height
? $this->Fit($width, $height)
: $this;
}
/**
* Scale image proportionally by width. Use in templates with $ScaleWidth.
*
* @param integer $width The width to set
* @return AssetContainer
*/
public function ScaleWidth($width)
{
if ($this->isWidth($width)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $width);
return $this->manipulateImage($variant, function (Image_Backend $backend) use ($width) {
return $backend->resizeByWidth($width);
});
}
/**
* Proportionally scale down this image if it is wider than the specified width.
* Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth.
*
* @uses ScalingManipulation::ScaleWidth()
* @param integer $width The maximum width of the output image
* @return AssetContainer
*/
public function ScaleMaxWidth($width)
{
return $this->getWidth() > $width
? $this->ScaleWidth($width)
: $this;
}
/**
* Scale image proportionally by height. Use in templates with $ScaleHeight.
*
* @param int $height The height to set
* @return AssetContainer
*/
public function ScaleHeight($height)
{
if ($this->isHeight($height)) {
return $this;
}
$variant = $this->variantName(__FUNCTION__, $height);
return $this->manipulateImage($variant, function (Image_Backend $backend) use ($height) {
return $backend->resizeByHeight($height);
});
}
/**
* Proportionally scale down this image if it is taller than the specified height.
* Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight.
*
* @uses ScalingManipulation::ScaleHeight()
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function ScaleMaxHeight($height)
{
return $this->getHeight() > $height
? $this->ScaleHeight($height)
: $this;
}
/**
* Crop image on X axis if it exceeds specified width. Retain height.
* Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100)
*
* @uses CropManipulation::Fill()
* @param integer $width The maximum width of the output image
* @return AssetContainer
*/
public function CropWidth($width)
{
return $this->getWidth() > $width
? $this->Fill($width, $this->getHeight())
: $this;
}
/**
* Crop image on Y axis if it exceeds specified height. Retain width.
* Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100)
*
* @uses CropManipulation::Fill()
* @param integer $height The maximum height of the output image
* @return AssetContainer
*/
public function CropHeight($height)
{
return $this->getHeight() > $height
? $this->Fill($this->getWidth(), $height)
: $this;
}
/**
* Crop this image to the aspect ratio defined by the specified width and height,
* then scale down the image to those dimensions if it exceeds them.
* Similar to Fill but without up-sampling. Use in templates with $FillMax.
*
* @uses ImageManipulation::Fill()
* @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image
* @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image
* @return AssetContainer
*/
public function FillMax($width, $height)
{
// Prevent divide by zero on missing/blank file
if (!$this->getWidth() || !$this->getHeight()) {
return null;
}
// Is the image already the correct size?
if ($this->isSize($width, $height)) {
return $this;
}
// If not, make sure the image isn't upsampled
$imageRatio = $this->getWidth() / $this->getHeight();
$cropRatio = $width / $height;
// If cropping on the x axis compare heights
if ($cropRatio < $imageRatio && $this->getHeight() < $height) {
return $this->Fill($this->getHeight() * $cropRatio, $this->getHeight());
}
// Otherwise we're cropping on the y axis (or not cropping at all) so compare widths
if ($this->getWidth() < $width) {
return $this->Fill($this->getWidth(), $this->getWidth() / $cropRatio);
}
return $this->Fill($width, $height);
}
/**
* Resize and crop image to fill specified dimensions.
* Use in templates with $Fill
*
* @param integer $width Width to crop to
* @param integer $height Height to crop to
* @return AssetContainer
*/
public function Fill($width, $height)
{
if ($this->isSize($width, $height)) {
return $this;
}
// Resize
$variant = $this->variantName(__FUNCTION__, $width, $height);
return $this->manipulateImage($variant, function (Image_Backend $backend) use ($width, $height) {
return $backend->croppedResize($width, $height);
});
}
/**
* Default CMS thumbnail
*
* @return DBFile|DBHTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function CMSThumbnail()
{
$width = (int)Config::inst()->get(__CLASS__, 'cms_thumbnail_width');
$height = (int)Config::inst()->get(__CLASS__, 'cms_thumbnail_height');
return $this->ThumbnailIcon($width, $height);
}
/**
* Generates a thumbnail for use in the gridfield view
*
* @return AssetContainer|DBHTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function StripThumbnail()
{
$width = (int)Config::inst()->get(__CLASS__, 'strip_thumbnail_width');
$height = (int)Config::inst()->get(__CLASS__, 'strip_thumbnail_height');
return $this->ThumbnailIcon($width, $height);
}
/**
* Get preview for this file
*
* @return AssetContainer|DBHTMLText Either a resized thumbnail, or html for a thumbnail icon
*/
public function PreviewThumbnail()
{
$width = (int)Config::inst()->get(__CLASS__, 'asset_preview_width');
return $this->ScaleMaxWidth($width) ?: $this->IconTag();
}
/**
* Default thumbnail generation for Images
*
* @param int $width
* @param int $height
* @return AssetContainer
*/
public function Thumbnail($width, $height)
{
return $this->Pad($width, $height);
}
/**
* Thubnail generation for all file types.
*
* Resizes images, but returns an icon <img /> tag if this is not a resizable image
*
* @param int $width
* @param int $height
* @return AssetContainer|DBHTMLText
*/
public function ThumbnailIcon($width, $height)
{
return $this->Thumbnail($width, $height) ?: $this->IconTag();
}
/**
* Get HTML for img containing the icon for this file
*
* @return DBHTMLText
*/
public function IconTag()
{
return DBField::create_field(
'HTMLFragment',
'<img src="' . Convert::raw2att($this->getIcon()) . '" />'
);
}
/**
* Get URL to thumbnail of the given size.
*
* May fallback to default icon
*
* @param int $width
* @param int $height
* @return string
*/
public function ThumbnailURL($width, $height)
{
$thumbnail = $this->Thumbnail($width, $height);
if ($thumbnail) {
return $thumbnail->getURL();
}
return $this->getIcon();
}
/**
* Return the relative URL of an icon for the file type,
* based on the {@link appCategory()} value.
* Images are searched for in "framework/images/app_icons/".
*
* @return string URL to icon
*/
public function getIcon()
{
$filename = $this->getFilename();
$ext = pathinfo($filename, PATHINFO_EXTENSION);
return File::get_icon_for_extension($ext);
}
/**
* Get Image_Backend instance for this image
*
* @return Image_Backend
*/
public function getImageBackend()
{
if (!$this->getIsImage()) {
return null;
}
// Create backend for this object
/** @skipUpgrade */
return Injector::inst()->createWithArgs('Image_Backend', array($this));
}
/**
* Get the dimensions of this Image.
*
* @param string $dim One of the following:
* - "string": return the dimensions in string form
* - "array": it'll return the raw result
* - 0: return the height
* - 1: return the width
* @return string|int|array|null
*/
public function getDimensions($dim = "string")
{
if (!$this->getIsImage()) {
return null;
}
$content = $this->getString();
if (!$content) {
return null;
}
// Get raw content
$size = getimagesizefromstring($content);
if ($size === false) {
return null;
}
if ($dim === 'array') {
return $size;
}
// Get single dimension
if (is_numeric($dim)) {
return $size[$dim];
}
return "$size[0]x$size[1]";
}
/**
* Get the width of this image.
*
* @return int
*/
public function getWidth()
{
return $this->getDimensions(0);
}
/**
* Get the height of this image.
*
* @return int
*/
public function getHeight()
{
return $this->getDimensions(1);
}
/**
* Get the orientation of this image.
*
* @return int ORIENTATION_SQUARE | ORIENTATION_PORTRAIT | ORIENTATION_LANDSCAPE
*/
public function getOrientation()
{
$width = $this->getWidth();
$height = $this->getHeight();
if ($width > $height) {
return Image_Backend::ORIENTATION_LANDSCAPE;
} elseif ($height > $width) {
return Image_Backend::ORIENTATION_PORTRAIT;
} else {
return Image_Backend::ORIENTATION_SQUARE;
}
}
/**
* Determine if this image is of the specified size
*
* @param integer $width Width to check
* @param integer $height Height to check
* @return boolean
*/
public function isSize($width, $height)
{
return $this->isWidth($width) && $this->isHeight($height);
}
/**
* Determine if this image is of the specified width
*
* @param integer $width Width to check
* @return boolean
*/
public function isWidth($width)
{
if (empty($width) || !is_numeric($width)) {
throw new InvalidArgumentException("Invalid value for width");
}
return $this->getWidth() == $width;
}
/**
* Determine if this image is of the specified width
*
* @param integer $height Height to check
* @return boolean
*/
public function isHeight($height)
{
if (empty($height) || !is_numeric($height)) {
throw new InvalidArgumentException("Invalid value for height");
}
return $this->getHeight() == $height;
}
/**
* Wrapper for manipulate that passes in and stores Image_Backend objects instead of tuples
*
* @param string $variant
* @param callable $callback Callback which takes an Image_Backend object, and returns an Image_Backend result
* @return DBFile The manipulated file
*/
public function manipulateImage($variant, $callback)
{
return $this->manipulate(
$variant,
function (AssetStore $store, $filename, $hash, $variant) use ($callback) {
/** @var Image_Backend $backend */
$backend = $this->getImageBackend();
// If backend isn't available
if (!$backend || !$backend->getImageResource()) {
return null;
}
$backend = $callback($backend);
if (!$backend) {
return null;
}
$return = $backend->writeToStore(
$store,
$filename,
$hash,
$variant,
array('conflict' => AssetStore::CONFLICT_USE_EXISTING)
);
// Enforce garbage collection on $backend, avoid increasing memory use on each manipulation
// by holding on to the underlying GD image resource.
// Even though it's a local variable with no other references,
// PHP holds on to it for the entire lifecycle of the script,
// which is potentially related to passing it into the $callback closure.
gc_collect_cycles();
return $return;
}
);
}
/**
* Generate a new DBFile instance using the given callback if it hasn't been created yet, or
* return the existing one if it has.
*
* @param string $variant name of the variant to create
* @param callable $callback Callback which should return a new tuple as an array.
* This callback will be passed the backend, filename, hash, and variant
* This will not be called if the file does not
* need to be created.
* @return DBFile The manipulated file
*/
public function manipulate($variant, $callback)
{
// Verify this manipulation is applicable to this instance
if (!$this->exists()) {
return null;
}
// Build output tuple
$filename = $this->getFilename();
$hash = $this->getHash();
$existingVariant = $this->getVariant();
if ($existingVariant) {
$variant = $existingVariant . '_' . $variant;
}
// Skip empty files (e.g. Folder does not have a hash)
if (empty($filename) || empty($hash)) {
return null;
}
// Create this asset in the store if it doesn't already exist,
// otherwise use the existing variant
$store = Injector::inst()->get('AssetStore');
$result = null;
if (!$store->exists($filename, $hash, $variant)) {
$result = call_user_func($callback, $store, $filename, $hash, $variant);
} else {
$result = array(
'Filename' => $filename,
'Hash' => $hash,
'Variant' => $variant
);
}
// Callback may fail to perform this manipulation (e.g. resize on text file)
if (!$result) {
return null;
}
// Store result in new DBFile instance
/** @var DBFile $file */
$file = DBField::create_field('DBFile', $result);
return $file->setOriginal($this);
}
/**
* Name a variant based on a format with arbitrary parameters
*
* @param string $format The format name.
* @param mixed $arg,... Additional arguments
* @return string
* @throws InvalidArgumentException
*/
public function variantName($format, $arg = null)
{
$args = func_get_args();
array_shift($args);
return $format . Convert::base64url_encode($args);
}
}

View File

@ -1,151 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Storage\AssetStore;
/**
* Image_Backend
*
* A backend for manipulation of images via the Image class
*/
interface Image_Backend
{
/**
* Represents a square orientation
*/
const ORIENTATION_SQUARE = 0;
/**
* Represents a portrait orientation
*/
const ORIENTATION_PORTRAIT = 1;
/**
* Represents a landscape orientation
*/
const ORIENTATION_LANDSCAPE = 2;
/**
* Create a new backend with the given object
*
* @param AssetContainer $assetContainer Object to load from
*/
public function __construct(AssetContainer $assetContainer = null);
/**
* Populate the backend with a given object
*
* @param AssetContainer $assetContainer Object to load from
*/
public function loadFromContainer(AssetContainer $assetContainer);
/**
* Populate the backend from a local path
*
* @param string $path
*/
public function loadFrom($path);
/**
* Get the currently assigned image resource
*
* @return mixed
*/
public function getImageResource();
/**
* Write to the given asset store
*
* @param AssetStore $assetStore
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $config = array());
/**
* Write the backend to a local path
*
* @param string $path
*/
public function writeTo($path);
/**
* Set the quality to a value between 0 and 100
*
* @param int $quality
*/
public function setQuality($quality);
/**
* Resize an image, skewing it as necessary.
*
* @param int $width
* @param int $height
* @return static
*/
public function resize($width, $height);
/**
* Resize the image by preserving aspect ratio. By default, it will keep the image inside the maxWidth
* and maxHeight. Passing useAsMinimum will make the smaller dimension equal to the maximum corresponding dimension
*
* @param int $width
* @param int $height
* @param bool $useAsMinimum If true, image will be sized outside of these dimensions.
* If false (default) image will be sized inside these dimensions.
* @return static
*/
public function resizeRatio($width, $height, $useAsMinimum = false);
/**
* Resize an image by width. Preserves aspect ratio.
*
* @param int $width
* @return static
*/
public function resizeByWidth($width);
/**
* Resize an image by height. Preserves aspect ratio.
*
* @param int $height
* @return static
*/
public function resizeByHeight($height);
/**
* Return a clone of this image resized, with space filled in with the given colour
*
* @param int $width
* @param int $height
* @param string $backgroundColor
* @return static
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF");
/**
* Resize an image to cover the given width/height completely, and crop off any overhanging edges.
*
* @param int $width
* @param int $height
* @return static
*/
public function croppedResize($width, $height);
/**
* Crop's part of image.
* @param int $top y position of left upper corner of crop rectangle
* @param int $left x position of left upper corner of crop rectangle
* @param int $width rectangle width
* @param int $height rectangle height
* @return Image_Backend
*/
public function crop($top, $left, $width, $height);
}

View File

@ -1,285 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Core\Config\Config;
use Imagick;
use InvalidArgumentException;
use ImagickPixel;
if (!class_exists('Imagick')) {
return;
}
class ImagickBackend extends Imagick implements Image_Backend
{
/**
* @config
* @var int
*/
private static $default_quality = 75;
/**
* @return $this
*/
public function getImageResource()
{
// the object represents the resource
return $this;
}
/**
* Create a new backend with the given object
*
* @param AssetContainer $assetContainer Object to load from
*/
public function __construct(AssetContainer $assetContainer = null)
{
parent::__construct();
if ($assetContainer) {
$this->loadFromContainer($assetContainer);
}
}
public function loadFromContainer(AssetContainer $assetContainer)
{
$stream = $assetContainer->getStream();
$this->readImageFile($stream);
fclose($stream);
$this->setDefaultQuality();
}
public function loadFrom($path)
{
$this->readImage($path);
$this->setDefaultQuality();
}
protected function setDefaultQuality()
{
$this->setQuality(Config::inst()->get(__CLASS__, 'default_quality'));
}
public function writeToStore(AssetStore $assetStore, $filename, $hash = null, $variant = null, $config = array())
{
// Write to temporary file, taking care to maintain the extension
$path = tempnam(sys_get_temp_dir(), 'imagemagick');
if ($extension = pathinfo($filename, PATHINFO_EXTENSION)) {
$path .= "." . $extension;
}
$this->writeImage($path);
$result = $assetStore->setFromLocalFile($path, $filename, $hash, $variant, $config);
unlink($path);
return $result;
}
public function writeTo($path)
{
Filesystem::makeFolder(dirname($path));
if (is_dir(dirname($path))) {
$this->writeImage($path);
}
}
public function setQuality($quality)
{
$this->setImageCompressionQuality($quality);
}
public function resize($width, $height)
{
if (!$this->valid()) {
return null;
}
if ($width < 0 || $height < 0) {
throw new InvalidArgumentException("Image resizing dimensions cannot be negative");
}
if (!$width && !$height) {
throw new InvalidArgumentException("No dimensions given when resizing image");
}
if (!$width) {
throw new InvalidArgumentException("Width not given when resizing image");
}
if (!$height) {
throw new InvalidArgumentException("Height not given when resizing image");
}
//use whole numbers, ensuring that size is at least 1x1
$width = max(1, round($width));
$height = max(1, round($height));
$geometry = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width === $geometry["width"] && $height === $geometry["height"]) {
return $this;
}
$new = clone $this;
$new->resizeImage($width, $height, self::FILTER_LANCZOS, 1);
return $new;
}
public function resizeRatio($maxWidth, $maxHeight, $useAsMinimum = false)
{
if (!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
$widthRatio = $maxWidth / $geometry["width"];
$heightRatio = $maxHeight / $geometry["height"];
if ($widthRatio < $heightRatio) {
return $useAsMinimum
? $this->resizeByHeight($maxHeight)
: $this->resizeByWidth($maxWidth);
} else {
return $useAsMinimum
? $this->resizeByWidth($maxWidth)
: $this->resizeByHeight($maxHeight);
}
}
public function resizeByWidth($width)
{
if (!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
$heightScale = $width / $geometry["width"];
return $this->resize($width, $heightScale * $geometry["height"]);
}
public function resizeByHeight($height)
{
if (!$this->valid()) {
return null;
}
$geometry = $this->getImageGeometry();
$scale = $height / $geometry["height"];
return $this->resize($scale * $geometry["width"], $height);
}
/**
* paddedResize
*
* @param int $width
* @param int $height
* @param string $backgroundColor
* @param int $transparencyPercent
* @return Image_Backend
*/
public function paddedResize($width, $height, $backgroundColor = "FFFFFF", $transparencyPercent = 0)
{
if (!$this->valid()) {
return null;
}
//keep the % within bounds of 0-100
$transparencyPercent = min(100, max(0, $transparencyPercent));
$new = $this->resizeRatio($width, $height);
if ($transparencyPercent) {
$alphaHex = $this->calculateAlphaHex($transparencyPercent);
$new->setImageBackgroundColor("#{$backgroundColor}{$alphaHex}");
} else {
$new->setImageBackgroundColor("#{$backgroundColor}");
}
$w = $new->getImageWidth();
$h = $new->getImageHeight();
$new->extentImage($width, $height, ($w-$width)/2, ($h-$height)/2);
return $new;
}
/**
* Convert a percentage (or 'true') to a two char hex code to signifiy the level of an alpha channel
*
* @param $percent
* @return string
*/
public function calculateAlphaHex($percent)
{
if ($percent > 100) {
$percent = 100;
}
// unlike GD, this uses 255 instead of 127, and is reversed. Lower = more transparent
$alphaHex = dechex(255 - floor(255 * bcdiv($percent, 100, 2)));
if (strlen($alphaHex) == 1) {
$alphaHex = '0' .$alphaHex;
}
return $alphaHex;
}
/**
* croppedResize
*
* @param int $width
* @param int $height
* @return Image_Backend
*/
public function croppedResize($width, $height)
{
if (!$this->valid()) {
return null;
}
$width = round($width);
$height = round($height);
$geo = $this->getImageGeometry();
// Check that a resize is actually necessary.
if ($width == $geo["width"] && $height == $geo["height"]) {
return $this;
}
$new = clone $this;
$new->setBackgroundColor(new ImagickPixel('transparent'));
if (($geo['width']/$width) < ($geo['height']/$height)) {
$new->cropImage(
$geo['width'],
floor($height*$geo['width']/$width),
0,
($geo['height'] - ($height*$geo['width']/$width))/2
);
} else {
$new->cropImage(
ceil($width*$geo['height']/$height),
$geo['height'],
($geo['width'] - ($width*$geo['height']/$height))/2,
0
);
}
$new->thumbnailImage($width, $height, true);
return $new;
}
/**
* Crop's part of image.
* @param int $top y position of left upper corner of crop rectangle
* @param int $left x position of left upper corner of crop rectangle
* @param int $width rectangle width
* @param int $height rectangle height
* @return Image_Backend
*/
public function crop($top, $left, $width, $height)
{
$new = clone $this;
$new->cropImage($width, $height, $left, $top);
return $new;
}
}

View File

@ -1,187 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
/**
* Represents a container for a specific asset.
*
* This is used as a use-agnostic interface to a single asset backed by an AssetStore
*
* Note that there are no setter equivalents for each of getHash, getVariant and getFilename.
* User code should utilise the setFrom* methods instead.
*/
interface AssetContainer
{
/**
* Assign a set of data to the backend
*
* @param string $data Raw binary/text content
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array());
/**
* Assign a local file to the backend.
*
* @param string $path Absolute filesystem path to file
* @param string $filename Optional path to ask the backend to name as.
* Will default to the filename of the $path, excluding directories.
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the local file content.
*/
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array());
/**
* Assign a stream to the backend
*
* @param resource $stream Streamable resource
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the raw stream.
*/
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array());
/**
* @return string Data from the file in this container
*/
public function getString();
/**
* @return resource Data stream to the asset in this container
*/
public function getStream();
/**
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* If set to true, and the file is currently in protected mode, the asset store will ensure the
* returned URL is accessible for the duration of the current session / user.
* This will have no effect if the file is in published mode.
* This will not grant access to users other than the owner of the current session.
* @return string public url to the asset in this container
*/
public function getURL($grant = true);
/**
* @return string The absolute URL to the asset in this container
*/
public function getAbsoluteURL();
/**
* Get metadata for this file
*
* @return array|null File information
*/
public function getMetaData();
/**
* Get mime type
*
* @return string Mime type for this file
*/
public function getMimeType();
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize();
/**
* Determine if a valid non-empty image exists behind this asset
*
* @return bool
*/
public function getIsImage();
/**
* Determine visibility of the given file
*
* @return string one of values defined by the constants VISIBILITY_PROTECTED or VISIBILITY_PUBLIC, or
* null if the file does not exist
*/
public function getVisibility();
/**
* Determine if this container has a valid value
*
* @return bool Flag as to whether the file exists
*/
public function exists();
/**
* Get value of filename
*
* @return string
*/
public function getFilename();
/**
* Get value of hash
*
* @return string
*/
public function getHash();
/**
* Get value of variant
*
* @return string
*/
public function getVariant();
/**
* Delete a file (and all variants).
* {@see AssetStore::delete()}
*
* @return bool Flag if a file was deleted
*/
public function deleteFile();
/**
* Publicly expose the file (and all variants) identified by the given filename and hash
* {@see AssetStore::publish}
*/
public function publishFile();
/**
* Protect a file (and all variants) from public access, identified by the given filename and hash.
* {@see AssetStore::protect()}
*/
public function protectFile();
/**
* Ensures that access to the specified protected file is granted for the current user.
* If this file is currently in protected mode, the asset store will ensure the
* returned asset for the duration of the current session / user.
* This will have no effect if the file is in published mode.
* This will not grant access to users other than the owner of the current session.
* Does not require a member to be logged in.
*/
public function grantFile();
/**
* Revoke access to the given file for the current user.
* Note: This will have no effect if the given file is public
*/
public function revokeFile();
/**
* Check if the current user can view the given file.
*
* @return bool True if the file is verified and grants access to the current session / user.
*/
public function canViewFile();
}

View File

@ -1,27 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
/**
* Provides a mechanism for suggesting filename alterations to a file
*
* Does not actually check for existence of the file, but rather comes up with as many suggestions for
* the given file as possible to a finite limit.
*/
interface AssetNameGenerator extends \Iterator
{
/**
* Construct a generator for the given filename
*
* @param string $filename
*/
public function __construct($filename);
/**
* Number of attempts allowed
*
* @return int
*/
public function getMaxTries();
}

View File

@ -1,267 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
/**
* Represents an abstract asset persistence layer. Acts as a backend to files.
*
* Asset storage is identified by the following values arranged into a tuple:
*
* - "Filename" - Descriptive path for a file, although not necessarily a physical location. This could include
* custom directory names as a parent, as well as an extension.
* - "Hash" - The SHA1 of the file. This means that multiple files with the same Filename could be
* stored independently (depending on implementation) as long as they have different hashes.
* When a variant is identified, this value will refer to the hash of the file it was generated
* from, not the hash of the actual generated file.
* - "Variant" - An arbitrary string (which should not contain filesystem invalid characters) used
* to identify an asset which is a variant of an original. The asset storage backend has no knowledge
* of the mechanism used to generate this file, and is up to user code to perform the actual
* generation. An empty variant identifies this file as the original file.
*
* Write options have an additional $config parameter to provide additional options to the backend.
* This is an associative array. Standard array options include 'visibility' and 'conflict'.
*
* 'conflict' config option determines the conflict resolution mechanism.
* When assets are stored in the backend, user code may request one of the following conflict resolution
* mechanisms:
*
* - CONFLICT_OVERWRITE - If there is an existing file with this tuple, overwrite it.
* - CONFLICT_RENAME - If there is an existing file with this tuple, pick a new Filename for it and return it.
* This option is not allowed for use when storing variants, which should not modify the underlying
* Filename tuple value.
* - CONFLICT_USE_EXISTING - If there is an existing file with this tuple, return the tuple for the
* existing file instead.
* - CONFLICT_EXCEPTION - If there is an existing file with this tuple, throw an exception.
*
* 'visibility' config option determines whether the file should be marked as publicly visible.
* This may be assigned to one of the below values:
*
* - VISIBILITY_PUBLIC: This file may be accessed by any public user.
* - VISIBILITY_PROTECTED: This file must be whitelisted for individual users before being made available to that user.
*/
interface AssetStore
{
/**
* Exception on file conflict
*/
const CONFLICT_EXCEPTION = 'exception';
/**
* Overwrite on file conflict
*/
const CONFLICT_OVERWRITE = 'overwrite';
/**
* Rename on file conflict. Rename rules will be determined by the backend.
*
* This option is not allowed for use when storing variants, which should not modify the underlying
* Filename tuple value.
*/
const CONFLICT_RENAME = 'rename';
/**
* On conflict, use existing file
*/
const CONFLICT_USE_EXISTING = 'existing';
/**
* Protect this file
*/
const VISIBILITY_PROTECTED = 'protected';
/**
* Make this file public
*/
const VISIBILITY_PUBLIC = 'public';
/**
* Return list of feature capabilities of this backend as an array.
* Array keys will be the options supported by $config, and the
* values will be the list of accepted values for each option (or
* true if any value is allowed).
*
* @return array
*/
public function getCapabilities();
/**
* Assign a set of data to the backend
*
* @param string $data Raw binary/text content
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the given data.
*/
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array());
/**
* Assign a local file to the backend.
*
* @param string $path Absolute filesystem path to file
* @param string $filename Optional path to ask the backend to name as.
* Will default to the filename of the $path, excluding directories.
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the local file content.
*/
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array());
/**
* Assign a stream to the backend
*
* @param resource $stream Streamable resource
* @param string $filename Name for the resulting file
* @param string $hash Hash of original file, if storing a variant.
* @param string $variant Name of variant, if storing a variant.
* @param array $config Write options. {@see AssetStore}
* @return array Tuple associative array (Filename, Hash, Variant) Unless storing a variant, the hash
* will be calculated from the raw stream.
*/
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array());
/**
* Get contents of a given file
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return string Data from the file.
*/
public function getAsString($filename, $hash, $variant = null);
/**
* Get a stream for this file
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return resource Data stream
*/
public function getAsStream($filename, $hash, $variant = null);
/**
* Get the url for the file
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* If set to true, and the file is currently in protected mode, the asset store will ensure the
* returned URL is accessible for the duration of the current session / user.
* This will have no effect if the file is in published mode.
* This will not grant access to users other than the owner of the current session.
* @return string public url to this resource
*/
public function getAsURL($filename, $hash, $variant = null, $grant = true);
/**
* Get metadata for this file, if available
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return array|null File information, or null if no metadata available
*/
public function getMetadata($filename, $hash, $variant = null);
/**
* Get mime type of this file
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return string Mime type for this file
*/
public function getMimeType($filename, $hash, $variant = null);
/**
* Determine visibility of the given file
*
* @param string $filename
* @param string $hash
* @return string one of values defined by the constants VISIBILITY_PROTECTED or VISIBILITY_PUBLIC, or
* null if the file does not exist
*/
public function getVisibility($filename, $hash);
/**
* Determine if a file exists with the given tuple
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
* If a variant is requested, this is the hash of the file before it was modified.
* @param string|null $variant Optional variant string for this file
* @return bool Flag as to whether the file exists
*/
public function exists($filename, $hash, $variant = null);
/**
* Delete a file (and all variants) identified by the given filename and hash
*
* @param string $filename
* @param string $hash
* @return bool Flag if a file was deleted
*/
public function delete($filename, $hash);
/**
* Publicly expose the file (and all variants) identified by the given filename and hash
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
*/
public function publish($filename, $hash);
/**
* Protect a file (and all variants) from public access, identified by the given filename and hash.
*
* A protected file can be granted access to users on a per-session or per-user basis as response
* to any future invocations of {@see grant()} or {@see getAsURL()} with $grant = true
*
* @param string $filename Filename (not including assets)
* @param string $hash sha1 hash of the file content.
*/
public function protect($filename, $hash);
/**
* Ensures that access to the specified protected file is granted for the current user.
* If this file is currently in protected mode, the asset store will ensure the
* returned asset for the duration of the current session / user.
* This will have no effect if the file is in published mode.
* This will not grant access to users other than the owner of the current session.
* Does not require a member to be logged in.
*
* @param string $filename
* @param string $hash
*/
public function grant($filename, $hash);
/**
* Revoke access to the given file for the current user.
* Note: This will have no effect if the given file is public
*
* @param string $filename
* @param string $hash
*/
public function revoke($filename, $hash);
/**
* Check if the current user can view the given file.
*
* @param string $filename
* @param string $hash
* @return bool True if the file is verified and grants access to the current session / user.
*/
public function canView($filename, $hash);
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
/**
* Represents a store usable with ProtectedFileController to serve up non-direct file requests
*/
interface AssetStoreRouter
{
/**
* Generate a custom HTTP response for a request to a given asset, identified by a path.
*
*
* @param string $asset Asset path name, omitting any leading 'assets'
* @return HTTPResponse
* @throws HTTPResponse_Exception
*/
public function getResponseFor($asset);
}

View File

@ -1,563 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Thumbnail;
use SilverStripe\Assets\ImageManipulation;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Control\Director;
use SilverStripe\Forms\FileField;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\FieldType\DBComposite;
use SilverStripe\Security\Permission;
/**
* Represents a file reference stored in a database
*
* @property string $Hash SHA of the file
* @property string $Filename Name of the file, including directory
* @property string $Variant Variant of the file
*/
class DBFile extends DBComposite implements AssetContainer, Thumbnail
{
use ImageManipulation;
/**
* List of allowed file categories.
*
* {@see File::$app_categories}
*
* @var array
*/
protected $allowedCategories = array();
/**
* List of image mime types supported by the image manipulations API
*
* {@see File::app_categories} for matching extensions.
*
* @config
* @var array
*/
private static $supported_images = array(
'image/jpeg',
'image/gif',
'image/png'
);
/**
* Create a new image manipulation
*
* @param string $name
* @param array|string $allowed List of allowed file categories (not extensions), as per File::$app_categories
*/
public function __construct($name = null, $allowed = array())
{
parent::__construct($name);
$this->setAllowedCategories($allowed);
}
/**
* Determine if a valid non-empty image exists behind this asset, which is a format
* compatible with image manipulations
*
* @return boolean
*/
public function getIsImage()
{
// Check file type
$mime = $this->getMimeType();
return $mime && in_array($mime, $this->config()->supported_images);
}
/**
* @return AssetStore
*/
protected function getStore()
{
return Injector::inst()->get('AssetStore');
}
private static $composite_db = array(
"Hash" => "Varchar(255)", // SHA of the base content
"Filename" => "Varchar(255)", // Path identifier of the base content
"Variant" => "Varchar(255)", // Identifier of the variant to the base, if given
);
private static $casting = array(
'URL' => 'Varchar',
'AbsoluteURL' => 'Varchar',
'Basename' => 'Varchar',
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
'Tag' => 'HTMLFragment',
'Size' => 'Varchar'
);
public function scaffoldFormField($title = null, $params = null)
{
return null;
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function XML()
{
return $this->getTag() ?: '';
}
/**
* Return a html5 tag of the appropriate for this file (normally img or a)
*
* @return string
*/
public function getTag()
{
$template = $this->getFrontendTemplate();
if (empty($template)) {
return '';
}
return (string)$this->renderWith($template);
}
/**
* Determine the template to render as on the frontend
*
* @return string Name of template
*/
public function getFrontendTemplate()
{
// Check that path is available
$url = $this->getURL();
if (empty($url)) {
return null;
}
// Image template for supported images
if ($this->getIsImage()) {
return 'DBFile_image';
}
// Default download
return 'DBFile_download';
}
/**
* Get trailing part of filename
*
* @return string
*/
public function getBasename()
{
if (!$this->exists()) {
return null;
}
return basename($this->getSourceURL());
}
/**
* Get file extension
*
* @return string
*/
public function getExtension()
{
if (!$this->exists()) {
return null;
}
return pathinfo($this->Filename, PATHINFO_EXTENSION);
}
/**
* Alt title for this
*
* @return string
*/
public function getTitle()
{
// If customised, use the customised title
if ($this->failover && ($title = $this->failover->Title)) {
return $title;
}
// fallback to using base name
return $this->getBasename();
}
public function setFromLocalFile($path, $filename = null, $hash = null, $variant = null, $config = array())
{
$this->assertFilenameValid($filename ?: $path);
$result = $this
->getStore()
->setFromLocalFile($path, $filename, $hash, $variant, $config);
// Update from result
if ($result) {
$this->setValue($result);
}
return $result;
}
public function setFromStream($stream, $filename, $hash = null, $variant = null, $config = array())
{
$this->assertFilenameValid($filename);
$result = $this
->getStore()
->setFromStream($stream, $filename, $hash, $variant, $config);
// Update from result
if ($result) {
$this->setValue($result);
}
return $result;
}
public function setFromString($data, $filename, $hash = null, $variant = null, $config = array())
{
$this->assertFilenameValid($filename);
$result = $this
->getStore()
->setFromString($data, $filename, $hash, $variant, $config);
// Update from result
if ($result) {
$this->setValue($result);
}
return $result;
}
public function getStream()
{
if (!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsStream($this->Filename, $this->Hash, $this->Variant);
}
public function getString()
{
if (!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsString($this->Filename, $this->Hash, $this->Variant);
}
public function getURL($grant = true)
{
if (!$this->exists()) {
return null;
}
$url = $this->getSourceURL($grant);
$this->updateURL($url);
$this->extend('updateURL', $url);
return $url;
}
/**
* Get URL, but without resampling.
* Note that this will return the url even if the file does not exist.
*
* @param bool $grant Ensures that the url for any protected assets is granted for the current user.
* @return string
*/
public function getSourceURL($grant = true)
{
return $this
->getStore()
->getAsURL($this->Filename, $this->Hash, $this->Variant, $grant);
}
/**
* Get the absolute URL to this resource
*
* @return string
*/
public function getAbsoluteURL()
{
if (!$this->exists()) {
return null;
}
return Director::absoluteURL($this->getURL());
}
public function getMetaData()
{
if (!$this->exists()) {
return null;
}
return $this
->getStore()
->getMetadata($this->Filename, $this->Hash, $this->Variant);
}
public function getMimeType()
{
if (!$this->exists()) {
return null;
}
return $this
->getStore()
->getMimeType($this->Filename, $this->Hash, $this->Variant);
}
public function getValue()
{
if (!$this->exists()) {
return null;
}
return array(
'Filename' => $this->Filename,
'Hash' => $this->Hash,
'Variant' => $this->Variant
);
}
public function getVisibility()
{
if (empty($this->Filename)) {
return null;
}
return $this
->getStore()
->getVisibility($this->Filename, $this->Hash);
}
public function exists()
{
if (empty($this->Filename)) {
return false;
}
return $this
->getStore()
->exists($this->Filename, $this->Hash, $this->Variant);
}
public function getFilename()
{
return $this->getField('Filename');
}
public function getHash()
{
return $this->getField('Hash');
}
public function getVariant()
{
return $this->getField('Variant');
}
/**
* Return file size in bytes.
*
* @return int
*/
public function getAbsoluteSize()
{
$metadata = $this->getMetaData();
if (isset($metadata['size'])) {
return $metadata['size'];
}
return 0;
}
/**
* Customise this object with an "original" record for getting other customised fields
*
* @param AssetContainer $original
* @return $this
*/
public function setOriginal($original)
{
$this->failover = $original;
return $this;
}
/**
* Get list of allowed file categories
*
* @return array
*/
public function getAllowedCategories()
{
return $this->allowedCategories;
}
/**
* Assign allowed categories
*
* @param array|string $categories
* @return $this
*/
public function setAllowedCategories($categories)
{
if (is_string($categories)) {
$categories = preg_split('/\s*,\s*/', $categories);
}
$this->allowedCategories = (array)$categories;
return $this;
}
/**
* Gets the list of extensions (if limited) for this field. Empty list
* means there is no restriction on allowed types.
*
* @return array
*/
protected function getAllowedExtensions()
{
$categories = $this->getAllowedCategories();
return File::get_category_extensions($categories);
}
/**
* Validate that this DBFile accepts this filename as valid
*
* @param string $filename
* @throws ValidationException
* @return bool
*/
protected function isValidFilename($filename)
{
$extension = strtolower(File::get_file_extension($filename));
// Validate true if within the list of allowed extensions
$allowed = $this->getAllowedExtensions();
if ($allowed) {
return in_array($extension, $allowed);
}
// If no extensions are configured, fallback to global list
$globalList = File::config()->allowed_extensions;
if (in_array($extension, $globalList)) {
return true;
}
// Only admins can bypass global rules
return !File::config()->apply_restrictions_to_admin && Permission::check('ADMIN');
}
/**
* Check filename, and raise a ValidationException if invalid
*
* @param string $filename
* @throws ValidationException
*/
protected function assertFilenameValid($filename)
{
$result = new ValidationResult();
$this->validate($result, $filename);
if (!$result->isValid()) {
throw new ValidationException($result);
}
}
/**
* Hook to validate this record against a validation result
*
* @param ValidationResult $result
* @param string $filename Optional filename to validate. If omitted, the current value is validated.
* @return bool Valid flag
*/
public function validate(ValidationResult $result, $filename = null)
{
if (empty($filename)) {
$filename = $this->getFilename();
}
if (empty($filename) || $this->isValidFilename($filename)) {
return true;
}
$message = _t('File.INVALIDEXTENSIONSHORT', 'Extension is not allowed');
$result->addError($message);
return false;
}
public function setField($field, $value, $markChanged = true)
{
// Catch filename validation on direct assignment
if ($field === 'Filename' && $value) {
$this->assertFilenameValid($value);
}
return parent::setField($field, $value, $markChanged);
}
/**
* Returns the size of the file type in an appropriate format.
*
* @return string|false String value, or false if doesn't exist
*/
public function getSize()
{
$size = $this->getAbsoluteSize();
if ($size) {
return File::format_size($size);
}
return false;
}
public function deleteFile()
{
if (!$this->Filename) {
return false;
}
return $this
->getStore()
->delete($this->Filename, $this->Hash);
}
public function publishFile()
{
if ($this->Filename) {
$this
->getStore()
->publish($this->Filename, $this->Hash);
}
}
public function protectFile()
{
if ($this->Filename) {
$this
->getStore()
->protect($this->Filename, $this->Hash);
}
}
public function grantFile()
{
if ($this->Filename) {
$this
->getStore()
->grant($this->Filename, $this->Hash);
}
}
public function revokeFile()
{
if ($this->Filename) {
$this
->getStore()
->revoke($this->Filename, $this->Hash);
}
}
public function canViewFile()
{
return $this->Filename
&& $this
->getStore()
->canView($this->Filename, $this->Hash);
}
}

View File

@ -1,170 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
use SilverStripe\Core\Config\Config;
/**
* Basic filename renamer
*/
class DefaultAssetNameGenerator implements AssetNameGenerator
{
/**
* A prefix for the version number added to an uploaded file
* when a file with the same name already exists.
* Example using no prefix: IMG001.jpg becomes IMG2.jpg
* Example using '-v' prefix: IMG001.jpg becomes IMG001-v2.jpg
*
* @config
* @var string
*/
private static $version_prefix = '-v';
/**
* Original filename
*
* @var string
*/
protected $filename;
/**
* Directory
*
* @var string
*/
protected $directory;
/**
* Name without extension or directory
*
* @var string
*/
protected $name;
/**
* Extension (including leading period)
*
* @var string
*/
protected $extension;
/**
* Next version number to suggest
*
* @var int
*/
protected $version;
/**
* Maximum number to suggest
*
* @var int
*/
protected $max = 100;
/**
* Number of digits to prefix with 0, if padding
*
* @var int
*/
protected $padding = 0;
/**
* First version
*
* @var int
*/
protected $first = null;
public function __construct($filename)
{
$this->filename = $filename;
$this->directory = ltrim(dirname($filename), '.');
$name = basename($this->filename);
// Note: Unlike normal extensions, we want to split at the first period, not the last.
if (($pos = strpos($name, '.')) !== false) {
$this->extension = substr($name, $pos);
$name = substr($name, 0, $pos);
} else {
$this->extension = null;
}
// Extract version prefix if already applied to this file
$this->padding = 0;
$pattern = '/^(?<name>[^\/]+?)' . preg_quote($this->getPrefix()) . '(?<version>[0-9]+)$/';
if (preg_match($pattern, $name, $matches)) {
$this->first = (int)$matches['version'];
$this->name = $matches['name'];
// Check if number is padded
if (strpos($matches['version'], '0') === 0) {
$this->padding = strlen($matches['version']);
}
} else {
$this->first = 1;
$this->name = $name;
}
$this->rewind();
}
/**
* Get numeric prefix
*
* @return string
*/
protected function getPrefix()
{
return Config::inst()->get(__CLASS__, 'version_prefix');
}
public function current()
{
$version = $this->version;
// Initially suggest original name
if ($version === $this->first) {
return $this->filename;
}
// If there are more than $this->max files we need a new scheme
if ($version >= $this->max + $this->first - 1) {
$version = substr(md5(time()), 0, 10);
} elseif ($this->padding) {
// Else, pad
$version = str_pad($version, $this->padding, '0', STR_PAD_LEFT);
}
// Build next name
$filename = $this->name . $this->getPrefix() . $version . $this->extension;
if ($this->directory) {
$filename = $this->directory . DIRECTORY_SEPARATOR . $filename;
}
return $filename;
}
public function key()
{
return $this->version - $this->first;
}
public function next()
{
$this->version++;
}
public function rewind()
{
$this->version = $this->first;
}
public function valid()
{
return $this->version < $this->max + $this->first;
}
public function getMaxTries()
{
return $this->max;
}
}

View File

@ -1,53 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
/**
* Interface to define a handler for persistent generated files
*/
interface GeneratedAssetHandler
{
/**
* Returns a URL to a generated asset, if one is available.
*
* 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 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 getContentURL($filename, $callback = null);
/**
* Returns the content for a generated asset, if one is available.
*
* 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 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 getContent($filename, $callback = null);
/**
* Update content with new value
*
* @param string $filename
* @param string $content Content to write to the backend
*/
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);
}

View File

@ -1,100 +0,0 @@
<?php
namespace SilverStripe\Assets\Storage;
use SilverStripe\Assets\File;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
/**
* Provides routing for session-whitelisted protected files
*/
class ProtectedFileController extends Controller
{
/**
* Designated router
*
* @var AssetStoreRouter
*/
protected $handler = null;
/**
* @return AssetStoreRouter
*/
public function getRouteHandler()
{
return $this->handler;
}
/**
* @param AssetStoreRouter $handler
* @return $this
*/
public function setRouteHandler(AssetStoreRouter $handler)
{
$this->handler = $handler;
return $this;
}
private static $url_handlers = array(
'$Filename' => "handleFile"
);
private static $allowed_actions = array(
'handleFile'
);
/**
* Provide a response for the given file request
*
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function handleFile(HTTPRequest $request)
{
$filename = $this->parseFilename($request);
// Deny requests to private file
if (!$this->isValidFilename($filename)) {
return $this->httpError(400, "Invalid request");
}
// Pass through to backend
return $this->getRouteHandler()->getResponseFor($filename);
}
/**
* Check if the given filename is safe to pass to the route handler.
* This should block direct requests to assets/.protected/ paths
*
* @param $filename
* @return bool True if the filename is allowed
*/
public function isValidFilename($filename)
{
// Block hidden files
return !preg_match('#(^|[\\\\/])\\..*#', $filename);
}
/**
* Get the file component from the request
*
* @param HTTPRequest $request
* @return string
*/
protected function parseFilename(HTTPRequest $request)
{
$filename = '';
$next = $request->param('Filename');
while ($next) {
$filename = $filename ? File::join_paths($filename, $next) : $next;
$next = $request->shift();
}
if ($extension = $request->getExtension()) {
$filename = $filename . "." . $extension;
}
return $filename;
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\Assets;
/**
* An object which may have a thumbnail url
*/
interface Thumbnail
{
/**
* Get a thumbnail for this object
*
* @param int $width Preferred width of the thumbnail
* @param int $height Preferred height of the thumbnail
* @return string URL to the thumbnail, if available
*/
public function ThumbnailURL($width, $height);
}

View File

@ -1,437 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Storage\AssetNameGenerator;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Object;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use InvalidArgumentException;
use Exception;
/**
* Manages uploads via HTML forms processed by PHP,
* uploads to Silverstripe's default upload directory,
* and either creates a new or uses an existing File-object
* for syncing with the database.
*
* <b>Validation</b>
*
* By default, a user can upload files without extension limitations,
* which can be a security risk if the webserver is not properly secured.
* Use {@link setAllowedExtensions()} to limit this list,
* and ensure the "assets/" directory does not execute scripts
* (see http://doc.silverstripe.org/secure-development#filesystem).
* {@link File::$allowed_extensions} provides a good start for a list of "safe" extensions.
*
* @todo Allow for non-database uploads
*/
class Upload extends Controller
{
private static $allowed_actions = array(
'index',
'load'
);
/**
* A dataobject (typically {@see File}) which implements {@see AssetContainer}
*
* @var AssetContainer
*/
protected $file;
/**
* Validator for this upload field
*
* @var Upload_Validator
*/
protected $validator;
/**
* Information about the temporary file produced
* by the PHP-runtime.
*
* @var array
*/
protected $tmpFile;
/**
* Replace an existing file rather than renaming the new one.
*
* @var boolean
*/
protected $replaceFile = false;
/**
* Processing errors that can be evaluated,
* e.g. by Form-validation.
*
* @var array
*/
protected $errors = array();
/**
* Default visibility to assign uploaded files
*
* @var string
*/
protected $defaultVisibility = AssetStore::VISIBILITY_PROTECTED;
/**
* A foldername relative to /assets,
* where all uploaded files are stored by default.
*
* @config
* @var string
*/
private static $uploads_folder = "Uploads";
/**
* A prefix for the version number added to an uploaded file
* when a file with the same name already exists.
* Example using no prefix: IMG001.jpg becomes IMG2.jpg
* Example using '-v' prefix: IMG001.jpg becomes IMG001-v2.jpg
*
* @config
* @var string
*/
private static $version_prefix = '-v';
public function __construct()
{
parent::__construct();
$this->validator = Upload_Validator::create();
$this->replaceFile = self::config()->replaceFile;
}
public function index()
{
return $this->httpError(404); // no-op
}
/**
* Get current validator
*
* @return Upload_Validator $validator
*/
public function getValidator()
{
return $this->validator;
}
/**
* Set a different instance than {@link Upload_Validator}
* for this upload session.
*
* @param object $validator
*/
public function setValidator($validator)
{
$this->validator = $validator;
}
/**
* Get an asset renamer for the given filename.
*
* @param string $filename Path name
* @return AssetNameGenerator
*/
protected function getNameGenerator($filename)
{
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
}
/**
*
* @return AssetStore
*/
protected function getAssetStore()
{
return Injector::inst()->get('AssetStore');
}
/**
* Save an file passed from a form post into the AssetStore directly
*
* @param array $tmpFile Indexed array that PHP generated for every file it uploads.
* @param string|bool $folderPath Folder path relative to /assets
* @return array|false Either the tuple array, or false if the file could not be saved
*/
public function load($tmpFile, $folderPath = false)
{
// Validate filename
$filename = $this->getValidFilename($tmpFile, $folderPath);
if (!$filename) {
return false;
}
// Save file into backend
$result = $this->storeTempFile($tmpFile, $filename, $this->getAssetStore());
//to allow extensions to e.g. create a version after an upload
$this->extend('onAfterLoad', $result, $tmpFile);
return $result;
}
/**
* Save an file passed from a form post into this object.
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*
* @param array $tmpFile
* @param AssetContainer $file
* @param string|bool $folderPath
* @return bool True if the file was successfully saved into this record
* @throws Exception
*/
public function loadIntoFile($tmpFile, $file = null, $folderPath = false)
{
$this->file = $file;
// Validate filename
$filename = $this->getValidFilename($tmpFile, $folderPath);
if (!$filename) {
return false;
}
$filename = $this->resolveExistingFile($filename);
// Save changes to underlying record (if it's a DataObject)
$this->storeTempFile($tmpFile, $filename, $this->file);
if ($this->file instanceof DataObject) {
$this->file->write();
}
//to allow extensions to e.g. create a version after an upload
$this->file->extend('onAfterUpload');
$this->extend('onAfterLoadIntoFile', $this->file);
return true;
}
/**
* Assign this temporary file into the given destination
*
* @param array $tmpFile
* @param string $filename
* @param AssetContainer|AssetStore $container
* @return array
*/
protected function storeTempFile($tmpFile, $filename, $container)
{
// Save file into backend
$conflictResolution = $this->replaceFile
? AssetStore::CONFLICT_OVERWRITE
: AssetStore::CONFLICT_RENAME;
$config = array(
'conflict' => $conflictResolution,
'visibility' => $this->getDefaultVisibility()
);
return $container->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $config);
}
/**
* Given a temporary file and upload path, validate the file and determine the
* value of the 'Filename' tuple that should be used to store this asset.
*
* @param array $tmpFile
* @param string $folderPath
* @return string|false Value of filename tuple, or false if invalid
*/
protected function getValidFilename($tmpFile, $folderPath = null)
{
if (!is_array($tmpFile)) {
throw new InvalidArgumentException(
"Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype"
);
}
// Validate
$this->clearErrors();
$valid = $this->validate($tmpFile);
if (!$valid) {
return false;
}
// Clean filename
if (!$folderPath) {
$folderPath = $this->config()->uploads_folder;
}
$nameFilter = FileNameFilter::create();
$file = $nameFilter->filter($tmpFile['name']);
$filename = basename($file);
if ($folderPath) {
$filename = File::join_paths($folderPath, $filename);
}
return $filename;
}
/**
* Given a file and filename, ensure that file renaming / replacing rules are satisfied
*
* If replacing, this method may replace $this->file with an existing record to overwrite.
* If renaming, a new value for $filename may be returned
*
* @param string $filename
* @return string $filename A filename safe to write to
* @throws Exception
*/
protected function resolveExistingFile($filename)
{
// Create a new file record (or try to retrieve an existing one)
if (!$this->file) {
$fileClass = File::get_class_for_file_extension(
File::get_file_extension($filename)
);
$this->file = Object::create($fileClass);
}
// Skip this step if not writing File dataobjects
if (! ($this->file instanceof File)) {
return $filename;
}
// Check there is if existing file
$existing = File::find($filename);
// If replacing (or no file exists) confirm this filename is safe
if ($this->replaceFile || !$existing) {
// If replacing files, make sure to update the OwnerID
if (!$this->file->ID && $this->replaceFile && $existing) {
$this->file = $existing;
$this->file->OwnerID = Member::currentUserID();
}
// Filename won't change if replacing
return $filename;
}
// if filename already exists, version the filename (e.g. test.gif to test-v2.gif, test-v2.gif to test-v3.gif)
$renamer = $this->getNameGenerator($filename);
foreach ($renamer as $newName) {
if (!File::find($newName)) {
return $newName;
}
}
// Fail
$tries = $renamer->getMaxTries();
throw new Exception("Could not rename {$filename} with {$tries} tries");
}
/**
* @param bool $replace
*/
public function setReplaceFile($replace)
{
$this->replaceFile = $replace;
}
/**
* @return bool
*/
public function getReplaceFile()
{
return $this->replaceFile;
}
/**
* Container for all validation on the file
* (e.g. size and extension restrictions).
* Is NOT connected to the {Validator} classes,
* please have a look at {FileField->validate()}
* for an example implementation of external validation.
*
* @param array $tmpFile
* @return boolean
*/
public function validate($tmpFile)
{
$validator = $this->validator;
$validator->setTmpFile($tmpFile);
$isValid = $validator->validate();
if ($validator->getErrors()) {
$this->errors = array_merge($this->errors, $validator->getErrors());
}
return $isValid;
}
/**
* Get file-object, either generated from {load()},
* or manually set.
*
* @return AssetContainer
*/
public function getFile()
{
return $this->file;
}
/**
* Set a file-object (similiar to {loadIntoFile()})
*
* @param AssetContainer $file
*/
public function setFile(AssetContainer $file)
{
$this->file = $file;
}
/**
* Clear out all errors (mostly set by {loadUploaded()})
* including the validator's errors
*/
public function clearErrors()
{
$this->errors = array();
$this->validator->clearErrors();
}
/**
* Determines wether previous operations caused an error.
*
* @return boolean
*/
public function isError()
{
return (count($this->errors));
}
/**
* Return all errors that occurred while processing so far
* (mostly set by {loadUploaded()})
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Get default visibility for uploaded files. {@see AssetStore}
* One of the values of AssetStore::VISIBILITY_* constants
*
* @return string
*/
public function getDefaultVisibility()
{
return $this->defaultVisibility;
}
/**
* Assign default visibility for uploaded files. {@see AssetStore}
* One of the values of AssetStore::VISIBILITY_* constants
*
* @param string $visibility
* @return $this
*/
public function setDefaultVisibility($visibility)
{
$this->defaultVisibility = $visibility;
return $this;
}
}

View File

@ -1,313 +0,0 @@
<?php
namespace SilverStripe\Assets;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Dev\SapphireTest;
class Upload_Validator
{
use Injectable;
/**
* Contains a list of the max file sizes shared by
* all upload fields. This is then duplicated into the
* "allowedMaxFileSize" instance property on construct.
*
* @config
* @var array
*/
private static $default_max_file_size = array();
/**
* Information about the temporary file produced
* by the PHP-runtime.
*
* @var array
*/
protected $tmpFile;
protected $errors = array();
/**
* Restrict filesize for either all filetypes
* or a specific extension, with extension-name
* as array-key and the size-restriction in bytes as array-value.
*
* @var array
*/
public $allowedMaxFileSize = array();
/**
* @var array Collection of extensions.
* Extension-names are treated case-insensitive.
*
* Example:
* <code>
* array("jpg","GIF")
* </code>
*/
public $allowedExtensions = array();
/**
* Return all errors that occurred while validating
* the temporary file.
*
* @return array
*/
public function getErrors()
{
return $this->errors;
}
/**
* Clear out all errors
*/
public function clearErrors()
{
$this->errors = array();
}
/**
* Set information about temporary file produced by PHP.
* @param array $tmpFile
*/
public function setTmpFile($tmpFile)
{
$this->tmpFile = $tmpFile;
}
/**
* Get maximum file size for all or specified file extension.
*
* @param string $ext
* @return int Filesize in bytes
*/
public function getAllowedMaxFileSize($ext = null)
{
// Check if there is any defined instance max file sizes
if (empty($this->allowedMaxFileSize)) {
// Set default max file sizes if there isn't
$fileSize = Config::inst()->get(__CLASS__, 'default_max_file_size');
if ($fileSize) {
$this->setAllowedMaxFileSize($fileSize);
} else {
// When no default is present, use maximum set by PHP
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
$maxPost = File::ini2bytes(ini_get('post_max_size'));
$this->setAllowedMaxFileSize(min($maxUpload, $maxPost));
}
}
$ext = strtolower($ext);
if ($ext) {
if (isset($this->allowedMaxFileSize[$ext])) {
return $this->allowedMaxFileSize[$ext];
}
$category = File::get_app_category($ext);
if ($category && isset($this->allowedMaxFileSize['[' . $category . ']'])) {
return $this->allowedMaxFileSize['[' . $category . ']'];
}
}
return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false;
}
/**
* Set filesize maximums (in bytes or INI format).
* Automatically converts extensions to lowercase
* for easier matching.
*
* Example:
* <code>
* array('*' => 200, 'jpg' => 1000, '[doc]' => '5m')
* </code>
*
* @param array|int $rules
*/
public function setAllowedMaxFileSize($rules)
{
if (is_array($rules) && count($rules)) {
// make sure all extensions are lowercase
$rules = array_change_key_case($rules, CASE_LOWER);
$finalRules = array();
foreach ($rules as $rule => $value) {
if (is_numeric($value)) {
$tmpSize = $value;
} else {
$tmpSize = File::ini2bytes($value);
}
$finalRules[$rule] = (int)$tmpSize;
}
$this->allowedMaxFileSize = $finalRules;
} elseif (is_string($rules)) {
$this->allowedMaxFileSize['*'] = File::ini2bytes($rules);
} elseif ((int)$rules > 0) {
$this->allowedMaxFileSize['*'] = (int)$rules;
}
}
/**
* @return array
*/
public function getAllowedExtensions()
{
return $this->allowedExtensions;
}
/**
* Limit allowed file extensions. Empty by default, allowing all extensions.
* To allow files without an extension, use an empty string.
* See {@link File::$allowed_extensions} to get a good standard set of
* extensions that are typically not harmful in a webserver context.
* See {@link setAllowedMaxFileSize()} to limit file size by extension.
*
* @param array $rules List of extensions
*/
public function setAllowedExtensions($rules)
{
if (!is_array($rules)) {
return;
}
// make sure all rules are lowercase
foreach ($rules as &$rule) {
$rule = strtolower($rule);
}
$this->allowedExtensions = $rules;
}
/**
* Determines if the bytesize of an uploaded
* file is valid - can be defined on an
* extension-by-extension basis in {@link $allowedMaxFileSize}
*
* @return boolean
*/
public function isValidSize()
{
// If file was blocked via PHP for being excessive size, shortcut here
switch ($this->tmpFile['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return false;
}
$pathInfo = pathinfo($this->tmpFile['name']);
$extension = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : null;
$maxSize = $this->getAllowedMaxFileSize($extension);
return (!$this->tmpFile['size'] || !$maxSize || (int)$this->tmpFile['size'] < $maxSize);
}
/**
* Determine if this file is valid but empty
*
* @return bool
*/
public function isFileEmpty()
{
// Don't check file size for errors
if ($this->tmpFile['error'] !== UPLOAD_ERR_OK) {
return false;
}
return empty($this->tmpFile['size']);
}
/**
* Determines if the temporary file has a valid extension
* An empty string in the validation map indicates files without an extension.
* @return boolean
*/
public function isValidExtension()
{
$pathInfo = pathinfo($this->tmpFile['name']);
// Special case for filenames without an extension
if (!isset($pathInfo['extension'])) {
return in_array('', $this->allowedExtensions, true);
} else {
return (!count($this->allowedExtensions)
|| in_array(strtolower($pathInfo['extension']), $this->allowedExtensions));
}
}
/**
* Run through the rules for this validator checking against
* the temporary file set by {@link setTmpFile()} to see if
* the file is deemed valid or not.
*
* @return boolean
*/
public function validate()
{
// we don't validate for empty upload fields yet
if (empty($this->tmpFile['name'])) {
return true;
}
// Check file upload
if (!$this->isValidUpload()) {
$this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload');
return false;
}
// Check file isn't empty
if ($this->isFileEmpty()) {
$this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
return false;
}
// filesize validation
if (!$this->isValidSize()) {
$pathInfo = pathinfo($this->tmpFile['name']);
$ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : '';
$arg = File::format_size($this->getAllowedMaxFileSize($ext));
$this->errors[] = _t(
'File.TOOLARGE',
'Filesize is too large, maximum {size} allowed',
'Argument 1: Filesize (e.g. 1MB)',
array('size' => $arg)
);
return false;
}
// extension validation
if (!$this->isValidExtension()) {
$this->errors[] = _t(
'File.INVALIDEXTENSION_SHORT',
'Extension is not allowed'
);
return false;
}
return true;
}
/**
* Check that a valid file was given for upload (ignores file size)
*
* @return bool
*/
public function isValidUpload()
{
// Check file upload
if ($this->tmpFile['error'] === UPLOAD_ERR_NO_FILE) {
return false;
}
// Check if file is valid uploaded (with exception for unit testing)
// Note that some "max file size" errors leave "temp_name" empty, so don't fail on this.
$isRunningTests = (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test());
if (!empty($this->tmpFile['tmp_name']) && !is_uploaded_file($this->tmpFile['tmp_name']) && !$isRunningTests) {
return false;
}
return true;
}
}

View File

@ -36,7 +36,7 @@ trait Extensible
* @var array $extensions
* @config
*/
private static $extensions = null;
private static $extensions = [];
private static $classes_constructed = array();

View File

@ -2,7 +2,6 @@
namespace SilverStripe\ORM;
use SilverStripe\Assets\AssetControlExtension;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Object;
@ -20,7 +19,6 @@ use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Search\SearchContext;
use SilverStripe\ORM\Queries\SQLInsert;
use SilverStripe\ORM\Queries\SQLDelete;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBComposite;
@ -247,16 +245,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
'Created' => 'DBDatetime',
);
/**
* Core dataobject extensions
*
* @config
* @var array
*/
private static $extensions = [
'AssetControl' => AssetControlExtension::class,
];
/**
* Override table name for this class. If ignored will default to FQN of class.
* This option is not inheritable, and must be set on each class.

View File

@ -1 +0,0 @@
<a href="$URL.ATT" title="$Title" <% if $Basename %>download="$Basename.ATT"<% else %>download<% end_if %>/>

View File

@ -1 +0,0 @@
<img src="$URL.ATT" alt="$Title.ATT" />

View File

@ -1,28 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
<head>
<% base_tag %>
<title><%t Image_iframe_ss.TITLE 'Image Uploading Iframe' %></title>
</head>
<body>
<div class="mainblock" style="width: 290px;">
<% if $UseSimpleForm %>
$EditImageSimpleForm
<% else %>
$EditImageForm
<% end_if %>
</div>
<% if $Image.ID %>
<div class="mainblock" >
$Image.CMSThumbnail
<% if $DeleteImageForm %>
$DeleteImageForm
<% end_if %>
</div>
<% end_if %>
</body>
</html>

View File

@ -1,2 +0,0 @@
Deny from all
RewriteRule .* - [F]

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Configuration to block web access to secure folders
-->
<configuration>
<system.webServer>
<rewrite>
<rules>
<clear />
<rule name="BlockProtectedAssets" patternSyntax="Wildcard" stopProcessing="true">
<match url="*" />
<action type="CustomResponse" statusCode="403" statusReason="Forbidden: Access is denied." statusDescription="You do not have permission to view this directory or page using the credentials that you supplied." />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@ -1,27 +0,0 @@
#
# Whitelist appropriate assets files.
# This file is automatically generated via File.allowed_extensions configuration
# See AssetAdapter::renderTemplate() for reference.
#
<IfModule mod_rewrite.c>
SetEnv HTTP_MOD_REWRITE On
RewriteEngine On
# Disable PHP handler
RewriteCond %{REQUEST_URI} .(?i:php|phtml|php3|php4|php5|inc)$
RewriteRule .* - [F]
# Allow error pages
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule error[^\\\\/]*\\.html$ - [L]
# Block invalid file extensions
RewriteCond %{REQUEST_URI} !\\.(?i:<% loop $AllowedExtensions %>$Extension<% if not $Last %>|<% end_if %><% end_loop %>)$
RewriteRule .* - [F]
# Non existant files passed to requesthandler
RewriteCond %{REQUEST_URI} ^(.*)$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* ../framework/main.php?url=%1 [QSA]
</IfModule>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Configuration to whitelist appropriate asset files, for IIS.
This file is automatically generated via File.allowed_extensions configuration
-->
<configuration>
<system.webServer>
<security>
<requestFiltering>
<fileExtensions allowUnlisted="false" applyToWebDAV="true">
<% loop $AllowedExtensions %>
<add fileExtension=".{$Extension}" allowed="true" />
<% end_loop %>
</fileExtensions>
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="Secure and 404 File rewrite" stopProcessing="true">
<match url="^(.*)$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="../framework/main.php?url={R:1}" appendQueryString="true" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

View File

@ -1,230 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Tests\AssetControlExtensionTest\ArchivedObject;
use SilverStripe\Assets\Tests\AssetControlExtensionTest\TestObject;
use SilverStripe\Assets\Tests\AssetControlExtensionTest\VersionedObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
/**
* Tests {@see AssetControlExtension}
*/
class AssetControlExtensionTest extends SapphireTest
{
protected $extraDataObjects = array(
VersionedObject::class,
TestObject::class
);
public function setUp()
{
parent::setUp();
// Set backend and base url
Versioned::set_stage(Versioned::DRAFT);
TestAssetStore::activate('AssetControlExtensionTest');
$this->logInWithPermission('ADMIN');
// Setup fixture manually
$object1 = new AssetControlExtensionTest\VersionedObject();
$object1->Title = 'My object';
$fish1 = realpath(__DIR__ .'/../ORM/ImageTest/test-image-high-quality.jpg');
$object1->Header->setFromLocalFile($fish1, 'Header/MyObjectHeader.jpg');
$object1->Download->setFromString('file content', 'Documents/File.txt');
$object1->write();
$object1->publishSingle();
$object2 = new AssetControlExtensionTest\TestObject();
$object2->Title = 'Unversioned';
$object2->Image->setFromLocalFile($fish1, 'Images/BeautifulFish.jpg');
$object2->write();
$object3 = new AssetControlExtensionTest\ArchivedObject();
$object3->Title = 'Archived';
$object3->Header->setFromLocalFile($fish1, 'Archived/MyObjectHeader.jpg');
$object3->write();
$object3->publishSingle();
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testFileDelete()
{
Versioned::set_stage(Versioned::DRAFT);
/**
* @var VersionedObject $object1
*/
$object1 = AssetControlExtensionTest\VersionedObject::get()
->filter('Title', 'My object')
->first();
/**
* @var Object $object2
*/
$object2 = AssetControlExtensionTest\TestObject::get()
->filter('Title', 'Unversioned')
->first();
/**
* @var ArchivedObject $object3
*/
$object3 = AssetControlExtensionTest\ArchivedObject::get()
->filter('Title', 'Archived')
->first();
$this->assertTrue($object1->Download->exists());
$this->assertTrue($object1->Header->exists());
$this->assertTrue($object2->Image->exists());
$this->assertTrue($object3->Header->exists());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object1->Download->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object1->Header->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object2->Image->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object3->Header->getVisibility());
// Check live stage for versioned objects
$object1Live = Versioned::get_one_by_stage(
VersionedObject::class,
'Live',
array('"ID"' => $object1->ID)
);
$object3Live = Versioned::get_one_by_stage(
ArchivedObject::class,
'Live',
array('"ID"' => $object3->ID)
);
$this->assertTrue($object1Live->Download->exists());
$this->assertTrue($object1Live->Header->exists());
$this->assertTrue($object3Live->Header->exists());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object1Live->Download->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object1Live->Header->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object3Live->Header->getVisibility());
// Delete live records; Should cause versioned records to be protected
$object1Live->deleteFromStage('Live');
$object3Live->deleteFromStage('Live');
$this->assertTrue($object1->Download->exists());
$this->assertTrue($object1->Header->exists());
$this->assertTrue($object3->Header->exists());
$this->assertTrue($object1Live->Download->exists());
$this->assertTrue($object1Live->Header->exists());
$this->assertTrue($object3Live->Header->exists());
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object1->Download->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object1->Header->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object3->Header->getVisibility());
// Delete draft record; Should remove all records
// Archived assets only should remain
$object1->delete();
$object2->delete();
$object3->delete();
$this->assertFalse($object1->Download->exists());
$this->assertFalse($object1->Header->exists());
$this->assertFalse($object2->Image->exists());
$this->assertTrue($object3->Header->exists());
$this->assertFalse($object1Live->Download->exists());
$this->assertFalse($object1Live->Header->exists());
$this->assertTrue($object3Live->Header->exists());
$this->assertNull($object1->Download->getVisibility());
$this->assertNull($object1->Header->getVisibility());
$this->assertNull($object2->Image->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object3->Header->getVisibility());
}
/**
* Test files being replaced
*/
public function testReplaceFile()
{
Versioned::set_stage(Versioned::DRAFT);
/**
* @var VersionedObject $object1
*/
$object1 = AssetControlExtensionTest\VersionedObject::get()
->filter('Title', 'My object')
->first();
/**
* @var Object $object2
*/
$object2 = AssetControlExtensionTest\TestObject::get()
->filter('Title', 'Unversioned')
->first();
/**
* @var ArchivedObject $object3
*/
$object3 = AssetControlExtensionTest\ArchivedObject::get()
->filter('Title', 'Archived')
->first();
$object1TupleOld = $object1->Header->getValue();
$object2TupleOld = $object2->Image->getValue();
$object3TupleOld = $object3->Header->getValue();
// Replace image and write each to filesystem
$fish1 = realpath(__DIR__ .'/../ORM/ImageTest/test-image-high-quality.jpg');
$object1->Header->setFromLocalFile($fish1, 'Header/Replaced_MyObjectHeader.jpg');
$object1->write();
$object2->Image->setFromLocalFile($fish1, 'Images/Replaced_BeautifulFish.jpg');
$object2->write();
$object3->Header->setFromLocalFile($fish1, 'Archived/Replaced_MyObjectHeader.jpg');
$object3->write();
// Check that old published records are left public, but removed for unversioned object2
$this->assertEquals(
AssetStore::VISIBILITY_PUBLIC,
$this->getAssetStore()->getVisibility($object1TupleOld['Filename'], $object1TupleOld['Hash'])
);
$this->assertEquals(
null, // Old file is destroyed
$this->getAssetStore()->getVisibility($object2TupleOld['Filename'], $object2TupleOld['Hash'])
);
$this->assertEquals(
AssetStore::VISIBILITY_PUBLIC,
$this->getAssetStore()->getVisibility($object3TupleOld['Filename'], $object3TupleOld['Hash'])
);
// Check that visibility of new file is correct
// Note that $object2 has no canView() is true, so assets end up public
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object1->Header->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object2->Image->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object3->Header->getVisibility());
// Publish changes to versioned records
$object1->publishSingle();
$object3->publishSingle();
// After publishing, old object1 is deleted, but since object3 has archiving enabled,
// the orphaned file is intentionally left in the protected store
$this->assertEquals(
null,
$this->getAssetStore()->getVisibility($object1TupleOld['Filename'], $object1TupleOld['Hash'])
);
$this->assertEquals(
AssetStore::VISIBILITY_PROTECTED,
$this->getAssetStore()->getVisibility($object3TupleOld['Filename'], $object3TupleOld['Hash'])
);
// And after publish, all files are public
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object1->Header->getVisibility());
$this->assertEquals(AssetStore::VISIBILITY_PUBLIC, $object3->Header->getVisibility());
}
/**
* @return AssetStore
*/
protected function getAssetStore()
{
return Injector::inst()->get('AssetStore');
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\AssetControlExtensionTest;
use SilverStripe\Dev\TestOnly;
/**
* Versioned object that always archives its assets
*/
class ArchivedObject extends VersionedObject implements TestOnly
{
private static $keep_archived_assets = true;
private static $table_name = 'AssetControlExtensionTest_ArchivedObject';
}

View File

@ -1,33 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\AssetControlExtensionTest;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
/**
* A basic unversioned object
*
* @property string $Title
* @property DBFile $Image
*/
class TestObject extends DataObject implements TestOnly
{
private static $db = array(
'Title' => 'Varchar(255)',
'Image' => "DBFile('image/supported')"
);
private static $table_name = 'AssetControlExtensionTest_TestObject';
/**
* @param Member $member
* @return bool
*/
public function canView($member = null)
{
return true;
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\AssetControlExtensionTest;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Security\Member;
/**
* Versioned object with attached assets
*
* @property string $Title
* @property DBFile $Header
* @property DBFile $Download
* @mixin Versioned
*/
class VersionedObject extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class
);
private static $db = array(
'Title' => 'Varchar(255)',
'Header' => "DBFile('image/supported')",
'Download' => 'DBFile'
);
private static $table_name = 'AssetControlExtensionTest_VersionedObject';
/**
* @param Member $member
* @return bool
*/
public function canView($member = null)
{
if (!$member) {
$member = Member::currentUser();
}
// Expectation that versioned::canView will hide this object in draft
$result = $this->extendedCan('canView', $member);
if ($result !== null) {
return $result;
}
// Open to public
return true;
}
}

View File

@ -1,86 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\AssetManipulationList;
use SilverStripe\Dev\SapphireTest;
/**
* Tests set manipulations of groups of assets of differing visibilities
*/
class AssetManipulationListTest extends SapphireTest
{
public function testVisibility()
{
$set = new AssetManipulationList();
$file1 = ['Filename' => 'Test1.jpg', 'Hash' => '975677589962604d9e16b700cf84734f9dda2817'];
$file2 = ['Filename' => 'Test2.jpg', 'Hash' => '22af86a45ea56287437a12cf83aded5c077a5db5'];
$file3 = ['Filename' => 'DupeHash1.jpg', 'Hash' => 'f167433dd318e738281b845a07d7be2053b8c997'];
$file4 = ['Filename' => 'DupeName.jpg', 'Hash' => 'afde6577a034323959b7915f41ac8d1f53bc597f'];
$file5 = ['Filename' => 'DupeName.jpg', 'Hash' => '1e94b066e5aa16907d0e5e32556c7a2a0b692eb9'];
$file6 = ['Filename' => 'DupeHash2.jpg', 'Hash' => 'f167433dd318e738281b845a07d7be2053b8c997'];
// Non-overlapping assets remain in assigned sets
$this->assertTrue($set->addDeletedAsset($file1));
$this->assertTrue($set->addDeletedAsset($file2));
$this->assertTrue($set->addProtectedAsset($file3));
$this->assertTrue($set->addProtectedAsset($file4));
$this->assertTrue($set->addPublicAsset($file5));
$this->assertTrue($set->addPublicAsset($file6));
// Check initial state of list
$this->assertEquals(6, $this->countItems($set));
$this->assertContains($file1, $set->getDeletedAssets());
$this->assertContains($file2, $set->getDeletedAssets());
$this->assertContains($file3, $set->getProtectedAssets());
$this->assertContains($file4, $set->getProtectedAssets());
$this->assertContains($file5, $set->getPublicAssets());
$this->assertContains($file6, $set->getPublicAssets());
// Public or Protected assets will not be deleted
$this->assertFalse($set->addDeletedAsset($file3));
$this->assertFalse($set->addDeletedAsset($file4));
$this->assertFalse($set->addDeletedAsset($file5));
$this->assertFalse($set->addDeletedAsset($file6));
$this->assertEquals(6, $this->countItems($set));
$this->assertNotContains($file3, $set->getDeletedAssets());
$this->assertNotContains($file4, $set->getDeletedAssets());
$this->assertNotContains($file5, $set->getDeletedAssets());
$this->assertNotContains($file6, $set->getDeletedAssets());
// Adding records as protected will remove them from the deletion list, but
// not the public list
$this->assertTrue($set->addProtectedAsset($file1));
$this->assertFalse($set->addProtectedAsset($file5));
$this->assertEquals(6, $this->countItems($set));
$this->assertNotContains($file1, $set->getDeletedAssets());
$this->assertContains($file1, $set->getProtectedAssets());
$this->assertNotContains($file5, $set->getProtectedAssets());
$this->assertContains($file5, $set->getPublicAssets());
// Adding records as public will ensure they are not deleted or marked as protected
// Existing public assets won't be re-added
$this->assertTrue($set->addPublicAsset($file2));
$this->assertTrue($set->addPublicAsset($file4));
$this->assertFalse($set->addPublicAsset($file5));
$this->assertEquals(6, $this->countItems($set));
$this->assertNotContains($file2, $set->getDeletedAssets());
$this->assertNotContains($file2, $set->getProtectedAssets());
$this->assertContains($file2, $set->getPublicAssets());
$this->assertNotContains($file4, $set->getProtectedAssets());
$this->assertContains($file4, $set->getPublicAssets());
$this->assertContains($file5, $set->getPublicAssets());
}
/**
* Helper to count all items in a set
*
* @param AssetManipulationList $set
* @return int
*/
protected function countItems(AssetManipulationList $set)
{
return count($set->getPublicAssets()) + count($set->getProtectedAssets()) + count($set->getDeletedAssets());
}
}

View File

@ -1,136 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use InvalidArgumentException;
use SilverStripe\Assets\FileFinder;
use SilverStripe\Dev\SapphireTest;
/**
* Tests for the {@link SS_FileFinder} class.
*/
class FileFinderTest extends SapphireTest
{
protected $base;
public function __construct()
{
$this->base = __DIR__ . '/FileFinderTest';
parent::__construct();
}
public function testBasicOperation()
{
$this->assertFinderFinds(
new FileFinder(),
array(
'file1.txt',
'file2.txt',
'dir1/dir1file1.txt',
'dir1/dir1file2.txt',
'dir1/dir2/dir2file1.txt',
'dir1/dir2/dir3/dir3file1.txt'
)
);
}
/**
* @expectedException InvalidArgumentException
*/
public function testInvalidOptionThrowsException()
{
$finder = new FileFinder();
$finder->setOption('this_doesnt_exist', 'ok');
}
public function testFilenameRegex()
{
$finder = new FileFinder();
$finder->setOption('name_regex', '/file2\.txt$/');
$this->assertFinderFinds(
$finder,
array(
'file2.txt',
'dir1/dir1file2.txt'),
'The finder only returns files matching the name regex.'
);
}
public function testIgnoreFiles()
{
$finder = new FileFinder();
$finder->setOption('ignore_files', array('file1.txt', 'dir1file1.txt', 'dir2file1.txt'));
$this->assertFinderFinds(
$finder,
array(
'file2.txt',
'dir1/dir1file2.txt',
'dir1/dir2/dir3/dir3file1.txt'),
'The finder ignores files with the basename in the ignore_files setting.'
);
}
public function testIgnoreDirs()
{
$finder = new FileFinder();
$finder->setOption('ignore_dirs', array('dir2'));
$this->assertFinderFinds(
$finder,
array(
'file1.txt',
'file2.txt',
'dir1/dir1file1.txt',
'dir1/dir1file2.txt'),
'The finder ignores directories in ignore_dirs.'
);
}
public function testMinDepth()
{
$finder = new FileFinder();
$finder->setOption('min_depth', 2);
$this->assertFinderFinds(
$finder,
array(
'dir1/dir2/dir2file1.txt',
'dir1/dir2/dir3/dir3file1.txt'
),
'The finder respects the min depth setting.'
);
}
public function testMaxDepth()
{
$finder = new FileFinder();
$finder->setOption('max_depth', 1);
$this->assertFinderFinds(
$finder,
array(
'file1.txt',
'file2.txt',
'dir1/dir1file1.txt',
'dir1/dir1file2.txt'),
'The finder respects the max depth setting.'
);
}
public function assertFinderFinds(FileFinder $finder, $expect, $message = null)
{
$found = $finder->find($this->base);
foreach ($expect as $k => $file) {
$expect[$k] = "{$this->base}/$file";
}
sort($expect);
sort($found);
$this->assertEquals($expect, $found, $message);
}
}

View File

@ -1,101 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\FileMigrationHelper;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Tests\FileMigrationHelperTest\Extension;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
/**
* Ensures that File dataobjects can be safely migrated from 3.x
*/
class FileMigrationHelperTest extends SapphireTest
{
protected static $fixture_file = 'FileMigrationHelperTest.yml';
protected $requiredExtensions = array(
File::class => array(
Extension::class
)
);
/**
* get the BASE_PATH for this test
*
* @return string
*/
protected function getBasePath()
{
// Note that the actual filesystem base is the 'assets' subdirectory within this
return ASSETS_PATH . '/FileMigrationHelperTest';
}
public function setUp()
{
Config::nest(); // additional nesting here necessary
Config::inst()->update(File::class, 'migrate_legacy_file', false);
parent::setUp();
// Set backend root to /FileMigrationHelperTest/assets
TestAssetStore::activate('FileMigrationHelperTest/assets');
// Ensure that each file has a local record file in this new assets base
$from = FRAMEWORK_PATH . '/tests/php/ORM/ImageTest/test-image-low-quality.jpg';
foreach (File::get()->exclude('ClassName', Folder::class) as $file) {
$dest = TestAssetStore::base_path() . '/' . $file->generateFilename();
Filesystem::makeFolder(dirname($dest));
copy($from, $dest);
}
}
public function tearDown()
{
TestAssetStore::reset();
Filesystem::removeFolder($this->getBasePath());
parent::tearDown();
Config::unnest();
}
/**
* Test file migration
*/
public function testMigration()
{
// Prior to migration, check that each file has empty Filename / Hash properties
foreach (File::get()->exclude('ClassName', Folder::class) as $file) {
$filename = $file->generateFilename();
$this->assertNotEmpty($filename, "File {$file->Name} has a filename");
$this->assertEmpty($file->File->getFilename(), "File {$file->Name} has no DBFile filename");
$this->assertEmpty($file->File->getHash(), "File {$file->Name} has no hash");
$this->assertFalse($file->exists(), "File with name {$file->Name} does not yet exist");
$this->assertFalse($file->isPublished(), "File is not published yet");
}
// Do migration
$helper = new FileMigrationHelper();
$result = $helper->run($this->getBasePath());
$this->assertEquals(5, $result);
// Test that each file exists
foreach (File::get()->exclude('ClassName', Folder::class) as $file) {
$expectedFilename = $file->generateFilename();
$filename = $file->File->getFilename();
$this->assertTrue($file->exists(), "File with name {$filename} exists");
$this->assertNotEmpty($filename, "File {$file->Name} has a Filename");
$this->assertEquals($expectedFilename, $filename, "File {$file->Name} has retained its Filename value");
$this->assertEquals(
'33be1b95cba0358fe54e8b13532162d52f97421c',
$file->File->getHash(),
"File with name {$filename} has the correct hash"
);
$this->assertTrue($file->isPublished(), "File is published after migration");
}
}
}

View File

@ -1,21 +0,0 @@
SilverStripe\Assets\Folder:
parent:
Name: ParentFolder
subfolder:
Name: SubFolder
Parent: =>SilverStripe\Assets\Folder.parent
SilverStripe\Assets\Image:
image1:
Name: myimage.jpg
image2:
Name: myimage.jpg
ParentID: =>SilverStripe\Assets\Folder.subfolder
SilverStripe\Assets\File:
file1:
Name: anotherfile.jpg
file2:
Name: file.jpg
ParentID: =>SilverStripe\Assets\Folder.parent
file3:
Name: picture.jpg
ParentID: =>SilverStripe\Assets\Folder.subfolder

View File

@ -1,26 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\FileMigrationHelperTest;
use SilverStripe\Assets\File;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataExtension;
/**
* @property File $owner
*/
class Extension extends DataExtension implements TestOnly
{
/**
* Ensure that File dataobject has the legacy "Filename" field
*/
private static $db = array(
"Filename" => "Text",
);
public function onBeforeWrite()
{
// Ensure underlying filename field is written to the database
$this->owner->setField('Filename', 'assets/' . $this->owner->generateFilename());
}
}

View File

@ -1,152 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\FileNameFilter;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\View\Parsers\Transliterator;
class FileNameFilterTest extends SapphireTest
{
public function setUp()
{
parent::setUp();
Config::inst()->update(
'SilverStripe\\Assets\\FileNameFilter',
'default_replacements',
array(
'/\s/' => '-', // remove whitespace
'/_/' => '-', // underscores to dashes
'/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
'/[\-]{2,}/' => '-', // remove duplicate dashes
'/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
)
);
}
public function testFilter()
{
$name = 'Brötchen für allë-mit_Unterstrich!.jpg';
$filter = new FileNameFilter();
$filter->setTransliterator(false);
$this->assertEquals(
'Brtchen-fr-all-mit-Unterstrich.jpg',
$filter->filter($name)
);
}
public function testFilterWithTransliterator()
{
$name = 'Brötchen für allë-mit_Unterstrich!.jpg';
$filter = new FileNameFilter();
$filter->setTransliterator(new Transliterator());
$this->assertEquals(
'Broetchen-fuer-alle-mit-Unterstrich.jpg',
$filter->filter($name)
);
}
public function testFilterWithCustomRules()
{
$name = 'Kuchen ist besser.jpg';
$filter = new FileNameFilter();
$filter->setTransliterator(false);
$filter->setReplacements(array('/[\s-]/' => '_'));
$this->assertEquals(
'Kuchen_ist_besser.jpg',
$filter->filter($name)
);
}
public function testFilterWithEmptyString()
{
$name = 'ö ö ö.jpg';
$filter = new FileNameFilter();
$filter->setTransliterator(new Transliterator());
$result = $filter->filter($name);
$this->assertFalse(
empty($result)
);
$this->assertStringEndsWith(
'.jpg',
$result
);
$this->assertGreaterThan(
strlen('.jpg'),
strlen($result)
);
}
public function testUnderscoresStartOfNameRemoved()
{
$name = '_test.txt';
$filter = new FileNameFilter();
$this->assertEquals('test.txt', $filter->filter($name));
}
public function testDoubleUnderscoresStartOfNameRemoved()
{
$name = '__test.txt';
$filter = new FileNameFilter();
$this->assertEquals('test.txt', $filter->filter($name));
}
public function testDotsStartOfNameRemoved()
{
$name = '.test.txt';
$filter = new FileNameFilter();
$this->assertEquals('test.txt', $filter->filter($name));
}
public function testDoubleDotsStartOfNameRemoved()
{
$name = '..test.txt';
$filter = new FileNameFilter();
$this->assertEquals('test.txt', $filter->filter($name));
}
public function testMixedInvalidCharsStartOfNameRemoved()
{
$name = '..#@$#@$^__test.txt';
$filter = new FileNameFilter();
$this->assertEquals('test.txt', $filter->filter($name));
}
public function testWhitespaceRemoved()
{
$name = ' test doc.txt';
$filter = new FileNameFilter();
$this->assertEquals('test-doc.txt', $filter->filter($name));
}
public function testUnderscoresReplacedWithDashes()
{
$name = 'test_doc.txt';
$filter = new FileNameFilter();
$this->assertEquals('test-doc.txt', $filter->filter($name));
}
public function testNonAsciiCharsReplacedWithDashes()
{
$name = '!@#$%^test_123@##@$#%^.txt';
$filter = new FileNameFilter();
$this->assertEquals('test-123.txt', $filter->filter($name));
}
public function testDuplicateDashesRemoved()
{
$name = 'test--document.txt';
$filter = new FileNameFilter();
$this->assertEquals('test-document.txt', $filter->filter($name));
}
public function testDoesntAddExtensionWhenMissing()
{
$name = 'no-extension';
$filter = new FileNameFilter();
$this->assertEquals('no-extension', $filter->filter($name));
}
}

View File

@ -1,714 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\Image;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Tests\FileTest\MyCustomFile;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\CMS\Model\ErrorPage;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\File;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Session;
use SilverStripe\Control\Director;
use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
/**
* Tests for the File class
*/
class FileTest extends SapphireTest
{
protected static $fixture_file = 'FileTest.yml';
protected $extraDataObjects = array(
MyCustomFile::class
);
public function setUp()
{
parent::setUp();
$this->logInWithPermission('ADMIN');
Versioned::set_stage(Versioned::DRAFT);
// Set backend root to /ImageTest
TestAssetStore::activate('FileTest');
// Create a test folders for each of the fixture references
$folderIDs = $this->allFixtureIDs(Folder::class);
foreach ($folderIDs as $folderID) {
$folder = DataObject::get_by_id(Folder::class, $folderID);
$filePath = ASSETS_PATH . '/FileTest/' . $folder->getFilename();
Filesystem::makeFolder($filePath);
}
// Create a test files for each of the fixture references
$fileIDs = $this->allFixtureIDs(File::class);
foreach ($fileIDs as $fileID) {
/**
* @var File $file
*/
$file = DataObject::get_by_id(File::class, $fileID);
$root = ASSETS_PATH . '/FileTest/';
if ($folder = $file->Parent()) {
$root .= $folder->getFilename();
}
$path = $root . substr($file->getHash(), 0, 10) . '/' . basename($file->getFilename());
Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
}
// Conditional fixture creation in case the 'cms' module is installed
if (class_exists('SilverStripe\\CMS\\Model\\ErrorPage')) {
$page = new ErrorPage(
array(
'Title' => 'Page not Found',
'ErrorCode' => 404
)
);
$page->write();
$page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
}
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testLinkShortcodeHandler()
{
$testFile = $this->objFromFixture(File::class, 'asdf');
$parser = new ShortcodeParser();
$parser->register('file_link', array(File::class, 'handle_shortcode'));
$fileShortcode = sprintf('[file_link,id=%d]', $testFile->ID);
$fileEnclosed = sprintf('[file_link,id=%d]Example Content[/file_link]', $testFile->ID);
$fileShortcodeExpected = $testFile->Link();
$fileEnclosedExpected = sprintf(
'<a href="%s" class="file" data-type="txt" data-size="977 KB">Example Content</a>',
$testFile->Link()
);
$this->assertEquals($fileShortcodeExpected, $parser->parse($fileShortcode), 'Test that simple linking works.');
$this->assertEquals($fileEnclosedExpected, $parser->parse($fileEnclosed), 'Test enclosed content is linked.');
$testFile->delete();
$fileShortcode = '[file_link,id="-1"]';
$fileEnclosed = '[file_link,id="-1"]Example Content[/file_link]';
$this->assertEquals('', $parser->parse('[file_link]'), 'Test that invalid ID attributes are not parsed.');
$this->assertEquals('', $parser->parse('[file_link,id="text"]'));
$this->assertEquals('', $parser->parse('[file_link]Example Content[/file_link]'));
if (class_exists('SilverStripe\\CMS\\Model\\ErrorPage')) {
$errorPage = ErrorPage::get()->filter('ErrorCode', 404)->first();
$this->assertEquals(
$errorPage->Link(),
$parser->parse($fileShortcode),
'Test link to 404 page if no suitable matches.'
);
$this->assertEquals(
sprintf('<a href="%s">Example Content</a>', $errorPage->Link()),
$parser->parse($fileEnclosed)
);
} else {
$this->assertEquals(
'',
$parser->parse($fileShortcode),
'Short code is removed if file record is not present.'
);
$this->assertEquals('', $parser->parse($fileEnclosed));
}
}
public function testCreateWithFilenameWithSubfolder()
{
// Note: We can't use fixtures/setUp() for this, as we want to create the db record manually.
// Creating the folder is necessary to avoid having "Filename" overwritten by setName()/setRelativePath(),
// because the parent folders don't exist in the database
$folder = Folder::find_or_make('/FileTest/');
$testfilePath = BASE_PATH . '/assets/FileTest/CreateWithFilenameHasCorrectPath.txt'; // Important: No leading slash
$fh = fopen($testfilePath, 'w');
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
$file = new File();
$file->setFromLocalFile($testfilePath);
$file->ParentID = $folder->ID;
$file->write();
$this->assertEquals(
'CreateWithFilenameHasCorrectPath.txt',
$file->Name,
'"Name" property is automatically set from "Filename"'
);
$this->assertEquals(
'FileTest/CreateWithFilenameHasCorrectPath.txt',
$file->Filename,
'"Filename" property remains unchanged'
);
// TODO This should be auto-detected, see File->updateFilesystem()
// $this->assertInstanceOf('Folder', $file->Parent(), 'Parent folder is created in database');
// $this->assertFileExists($file->Parent()->getURL(), 'Parent folder is created on filesystem');
// $this->assertEquals('FileTest', $file->Parent()->Name);
// $this->assertInstanceOf('Folder', $file->Parent()->Parent(), 'Grandparent folder is created in database');
// $this->assertFileExists($file->Parent()->Parent()->getURL(),
// 'Grandparent folder is created on filesystem');
// $this->assertEquals('assets', $file->Parent()->Parent()->Name);
}
public function testGetExtension()
{
$this->assertEquals(
'',
File::get_file_extension('myfile'),
'No extension'
);
$this->assertEquals(
'txt',
File::get_file_extension('myfile.txt'),
'Simple extension'
);
$this->assertEquals(
'gz',
File::get_file_extension('myfile.tar.gz'),
'Double-barrelled extension only returns last bit'
);
}
public function testValidateExtension()
{
Session::set('loggedInAs', null);
$orig = Config::inst()->get(File::class, 'allowed_extensions');
Config::inst()->remove(File::class, 'allowed_extensions');
Config::inst()->update(File::class, 'allowed_extensions', array('txt'));
$file = $this->objFromFixture(File::class, 'asdf');
// Invalid ext
$file->Name = 'asdf.php';
$result = $file->validate();
$this->assertFalse($result->isValid());
$messages = $result->getMessages();
$this->assertEquals(1, count($messages));
$this->assertEquals('Extension is not allowed', $messages[0]['message']);
// Valid ext
$file->Name = 'asdf.txt';
$result = $file->validate();
$this->assertTrue($result->isValid());
// Capital extension is valid as well
$file->Name = 'asdf.TXT';
$result = $file->validate();
$this->assertTrue($result->isValid());
}
public function testAppCategory()
{
// Test various categories
$this->assertEquals('image', File::get_app_category('jpg'));
$this->assertEquals('image', File::get_app_category('JPG'));
$this->assertEquals('image', File::get_app_category('JPEG'));
$this->assertEquals('image', File::get_app_category('png'));
$this->assertEquals('image', File::get_app_category('tif'));
$this->assertEquals('document', File::get_app_category('pdf'));
$this->assertEquals('video', File::get_app_category('mov'));
$this->assertEquals('audio', File::get_app_category('OGG'));
}
public function testGetCategoryExtensions()
{
// Test specific categories
$images = array(
'alpha', 'als', 'bmp', 'cel', 'gif', 'ico', 'icon', 'jpeg', 'jpg', 'pcx', 'png', 'ps', 'tif', 'tiff'
);
$this->assertEquals($images, File::get_category_extensions('image'));
$this->assertEquals(array('gif', 'jpeg', 'jpg', 'png'), File::get_category_extensions('image/supported'));
$this->assertEquals($images, File::get_category_extensions(array('image', 'image/supported')));
$this->assertEquals(
array('fla', 'gif', 'jpeg', 'jpg', 'png', 'swf'),
File::get_category_extensions(array('flash', 'image/supported'))
);
// Test other categories have at least one item
$this->assertNotEmpty(File::get_category_extensions('archive'));
$this->assertNotEmpty(File::get_category_extensions('audio'));
$this->assertNotEmpty(File::get_category_extensions('document'));
$this->assertNotEmpty(File::get_category_extensions('flash'));
$this->assertNotEmpty(File::get_category_extensions('video'));
}
/**
* @dataProvider allowedExtensions
* @param string $extension
*/
public function testAllFilesHaveCategory($extension)
{
$this->assertNotEmpty(
File::get_app_category($extension),
"Assert that extension {$extension} has a valid category"
);
}
/**
* Gets the list of all extensions for testing
*
* @return array
*/
public function allowedExtensions()
{
$args = array();
foreach (array_filter(File::config()->allowed_extensions) as $ext) {
$args[] = array($ext);
}
return $args;
}
public function testSetNameChangesFilesystemOnWrite()
{
/**
* @var File $file
*/
$file = $this->objFromFixture(File::class, 'asdf');
$this->logInWithPermission('ADMIN');
$file->publishRecursive();
$oldTuple = $file->File->getValue();
// Rename
$file->Name = 'renamed.txt';
$newTuple = $oldTuple;
$newTuple['Filename'] = $file->generateFilename();
// Before write()
$this->assertTrue(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old path is still present'
);
$this->assertFalse(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is updated in memory, not written before write() is called'
);
// After write()
$file->write();
$this->assertTrue(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old path exists after draft change'
);
$this->assertTrue(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is created after write()'
);
// After publish
$file->publishRecursive();
$this->assertFalse(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old file is finally removed after publishing new file'
);
$this->assertTrue(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is created after write()'
);
}
public function testSetParentIDChangesFilesystemOnWrite()
{
$file = $this->objFromFixture(File::class, 'asdf');
$this->logInWithPermission('ADMIN');
$file->publishRecursive();
$subfolder = $this->objFromFixture(Folder::class, 'subfolder');
$oldTuple = $file->File->getValue();
// set ParentID
$file->ParentID = $subfolder->ID;
$newTuple = $oldTuple;
$newTuple['Filename'] = $file->generateFilename();
// Before write()
$this->assertTrue(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old path is still present'
);
$this->assertFalse(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is updated in memory, not written before write() is called'
);
$file->write();
// After write()
$file->write();
$this->assertTrue(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old path exists after draft change'
);
$this->assertTrue(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is created after write()'
);
// After publish
$file->publishSingle();
$this->assertFalse(
$this->getAssetStore()->exists($oldTuple['Filename'], $oldTuple['Hash']),
'Old file is finally removed after publishing new file'
);
$this->assertTrue(
$this->getAssetStore()->exists($newTuple['Filename'], $newTuple['Hash']),
'New path is created after write()'
);
}
/**
* @see http://open.silverstripe.org/ticket/5693
*/
public function testSetNameWithInvalidExtensionDoesntChangeFilesystem()
{
Config::inst()->remove(File::class, 'allowed_extensions');
Config::inst()->update(File::class, 'allowed_extensions', array('txt'));
$this->setExpectedException(ValidationException::class);
$file = $this->objFromFixture(File::class, 'asdf');
$file->Name = 'renamed.php'; // evil extension
$file->write();
}
public function testGetURL()
{
$rootfile = $this->objFromFixture(File::class, 'asdf');
$this->assertEquals('/assets/FileTest/55b443b601/FileTest.txt', $rootfile->getURL());
}
public function testGetAbsoluteURL()
{
$rootfile = $this->objFromFixture(File::class, 'asdf');
$this->assertEquals(
Director::absoluteBaseURL() . 'assets/FileTest/55b443b601/FileTest.txt',
$rootfile->getAbsoluteURL()
);
}
public function testNameAndTitleGeneration()
{
// When name is assigned, title is automatically assigned
$file = $this->objFromFixture(Image::class, 'setfromname');
$this->assertEquals('FileTest', $file->Title);
}
public function testSizeAndAbsoluteSizeParameters()
{
$file = $this->objFromFixture(File::class, 'asdf');
/* AbsoluteSize will give the integer number */
$this->assertEquals(1000000, $file->AbsoluteSize);
/* Size will give a humanised number */
$this->assertEquals('977 KB', $file->Size);
}
public function testFileType()
{
$file = $this->objFromFixture(Image::class, 'gif');
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
$file = $this->objFromFixture(File::class, 'pdf');
$this->assertEquals("Adobe Acrobat PDF file", $file->FileType);
$file = $this->objFromFixture(Image::class, 'gifupper');
$this->assertEquals("GIF image - good for diagrams", $file->FileType);
/* Only a few file types are given special descriptions; the rest are unknown */
$file = $this->objFromFixture(File::class, 'asdf');
$this->assertEquals("unknown", $file->FileType);
}
/**
* Test the File::format_size() method
*/
public function testFormatSize()
{
$this->assertEquals("1000 bytes", File::format_size(1000));
$this->assertEquals("1023 bytes", File::format_size(1023));
$this->assertEquals("1 KB", File::format_size(1025));
$this->assertEquals("9.8 KB", File::format_size(10000));
$this->assertEquals("49 KB", File::format_size(50000));
$this->assertEquals("977 KB", File::format_size(1000000));
$this->assertEquals("1 MB", File::format_size(1024*1024));
$this->assertEquals("954 MB", File::format_size(1000000000));
$this->assertEquals("1 GB", File::format_size(1024*1024*1024));
$this->assertEquals("9.3 GB", File::format_size(10000000000));
// It use any denomination higher than GB. It also doesn't overflow with >32 bit integers
$this->assertEquals("93132.3 GB", File::format_size(100000000000000));
}
public function testDeleteFile()
{
/**
* @var File $file
*/
$file = $this->objFromFixture(File::class, 'asdf');
$this->logInWithPermission('ADMIN');
$file->publishSingle();
$tuple = $file->File->getValue();
// Before delete
$this->assertTrue(
$this->getAssetStore()->exists($tuple['Filename'], $tuple['Hash']),
'File is still present'
);
// after unpublish
$file->doUnpublish();
$this->assertTrue(
$this->getAssetStore()->exists($tuple['Filename'], $tuple['Hash']),
'File is still present after unpublish'
);
// after delete
$file->delete();
$this->assertFalse(
$this->getAssetStore()->exists($tuple['Filename'], $tuple['Hash']),
'File is deleted after unpublish and delete'
);
}
public function testRenameFolder()
{
$newTitle = "FileTest-folder-renamed";
//rename a folder's title
$folderID = $this->objFromFixture(Folder::class, "folder2")->ID;
$folder = DataObject::get_by_id(Folder::class, $folderID);
$folder->Title = $newTitle;
$folder->write();
//get folder again and see if the filename has changed
$folder = DataObject::get_by_id(Folder::class, $folderID);
$this->assertEquals(
$newTitle . '/',
$folder->Filename,
"Folder Filename updated after rename of Title"
);
//rename a folder's name
$newTitle2 = "FileTest-folder-renamed2";
$folder->Name = $newTitle2;
$folder->write();
//get folder again and see if the Title has changed
$folder = DataObject::get_by_id(Folder::class, $folderID);
$this->assertEquals(
$folder->Title,
$newTitle2,
"Folder Title updated after rename of Name"
);
//rename a folder's Filename
$newTitle3 = "FileTest-folder-renamed3";
$folder->Filename = $newTitle3;
$folder->write();
//get folder again and see if the Title has changed
$folder = DataObject::get_by_id(Folder::class, $folderID);
$this->assertEquals(
$folder->Title,
$newTitle3,
"Folder Title updated after rename of Filename"
);
}
public function testRenamesDuplicateFilesInSameFolder()
{
$original = new File();
$original->update([
'Name' => 'file1.txt',
'ParentID' => 0
]);
$original->write();
$duplicate = new File();
$duplicate->update([
'Name' => 'file1.txt',
'ParentID' => 0
]);
$duplicate->write();
$original = File::get()->byID($original->ID);
$this->assertEquals($original->Name, 'file1.txt');
$this->assertEquals($original->Title, 'file1');
$this->assertEquals($duplicate->Name, 'file1-v2.txt');
$this->assertEquals($duplicate->Title, 'file1 v2');
}
public function testSetsEmptyTitleToNameWithoutExtensionAndSpecialCharacters()
{
$fileWithTitle = new File();
$fileWithTitle->update([
'Name' => 'file1-with-title.txt',
'Title' => 'Some Title'
]);
$fileWithTitle->write();
$this->assertEquals($fileWithTitle->Name, 'file1-with-title.txt');
$this->assertEquals($fileWithTitle->Title, 'Some Title');
$fileWithoutTitle = new File();
$fileWithoutTitle->update([
'Name' => 'file1-without-title.txt',
]);
$fileWithoutTitle->write();
$this->assertEquals($fileWithoutTitle->Name, 'file1-without-title.txt');
$this->assertEquals($fileWithoutTitle->Title, 'file1 without title');
}
public function testSetsEmptyNameToSingularNameWithoutTitle()
{
$fileWithTitle = new File();
$fileWithTitle->update([
'Name' => '',
'Title' => 'Some Title',
]);
$fileWithTitle->write();
$this->assertEquals($fileWithTitle->Name, 'Some-Title');
$this->assertEquals($fileWithTitle->Title, 'Some Title');
$fileWithoutTitle = new File();
$fileWithoutTitle->update([
'Name' => '',
'Title' => '',
]);
$fileWithoutTitle->write();
$this->assertEquals($fileWithoutTitle->Name, $fileWithoutTitle->i18n_singular_name());
$this->assertEquals($fileWithoutTitle->Title, $fileWithoutTitle->i18n_singular_name());
}
public function testSetsEmptyNameToTitleIfPresent()
{
$file = new File();
$file->update([
'Name' => '',
'Title' => 'file1',
]);
$file->write();
$this->assertEquals($file->Name, 'file1');
$this->assertEquals($file->Title, 'file1');
}
public function testSetsOwnerOnFirstWrite()
{
Session::set('loggedInAs', null);
$member1 = new Member();
$member1->write();
$member2 = new Member();
$member2->write();
$file1 = new File();
$file1->write();
$this->assertEquals(0, $file1->OwnerID, 'Owner not written when no user is logged in');
$member1->logIn();
$file2 = new File();
$file2->write();
$this->assertEquals($member1->ID, $file2->OwnerID, 'Owner written when user is logged in');
$member2->logIn();
$file2->forceChange();
$file2->write();
$this->assertEquals($member1->ID, $file2->OwnerID, 'Owner not overwritten on existing files');
}
public function testCanEdit()
{
$file = $this->objFromFixture(Image::class, 'gif');
// Test anonymous permissions
Session::set('loggedInAs', null);
$this->assertFalse($file->canEdit(), "Anonymous users can't edit files");
// Test permissionless user
$this->objFromFixture(Member::class, 'frontend')->logIn();
$this->assertFalse($file->canEdit(), "Permissionless users can't edit files");
// Test global CMS section users
$this->objFromFixture(Member::class, 'cms')->logIn();
$this->assertTrue($file->canEdit(), "Users with all CMS section access can edit files");
// Test cms access users without file access
$this->objFromFixture(Member::class, 'security')->logIn();
$this->assertFalse($file->canEdit(), "Security CMS users can't edit files");
// Test asset-admin user
$this->objFromFixture(Member::class, 'assetadmin')->logIn();
$this->assertTrue($file->canEdit(), "Asset admin users can edit files");
// Test admin
$this->objFromFixture(Member::class, 'admin')->logIn();
$this->assertTrue($file->canEdit(), "Admins can edit files");
}
public function testJoinPaths()
{
$this->assertEquals('name/file.jpg', File::join_paths('/name', 'file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('name', 'file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('/name', '/file.jpg'));
$this->assertEquals('name/file.jpg', File::join_paths('name/', '/', 'file.jpg'));
$this->assertEquals('file.jpg', File::join_paths('/', '/', 'file.jpg'));
$this->assertEquals('', File::join_paths('/', '/'));
}
/**
* Test that ini2bytes returns the number of bytes for a PHP ini style size declaration
*
* @param string $iniValue
* @param int $expected
* @dataProvider ini2BytesProvider
*/
public function testIni2Bytes($iniValue, $expected)
{
$this->assertSame($expected, File::ini2bytes($iniValue));
}
/**
* @return array
*/
public function ini2BytesProvider()
{
return [
['2k', 2 * 1024],
['512M', 512 * 1024 * 1024],
['1024g', 1024 * 1024 * 1024 * 1024],
['1024G', 1024 * 1024 * 1024 * 1024]
];
}
/**
* @return AssetStore
*/
protected function getAssetStore()
{
return Injector::inst()->get('AssetStore');
}
}

View File

@ -1,84 +0,0 @@
SilverStripe\Assets\Folder:
subfolder:
Name: FileTest-subfolder
folder1:
Name: FileTest-folder1
folder2:
Name: FileTest-folder2
folder1-subfolder1:
Name: FileTest-folder1-subfolder1
ParentID: =>SilverStripe\Assets\Folder.folder1
SilverStripe\Assets\File:
asdf:
FileFilename: FileTest.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.txt
pdf:
FileFilename: FileTest.pdf
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.pdf
subfolderfile:
FileFilename: FileTest-subfolder/FileTestSubfolder.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTestSubfolder.txt
ParentID: =>SilverStripe\Assets\Folder.subfolder
subfolderfile-setfromname:
FileFilename: FileTest-subfolder/FileTestSubfolder2.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTestSubfolder2.txt
ParentID: =>SilverStripe\Assets\Folder.subfolder
file1-folder1:
FileFilename: FileTest-folder1/File1.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: File1.txt
ParentID: =>SilverStripe\Assets\Folder.folder1
SilverStripe\Assets\Image:
gif:
FileFilename: FileTest.gif
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.gif
gifupper:
FileFilename: FileTest-gifupper.GIF
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest-gifupper.GIF
setfromname:
FileFilename: FileTest.png
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: FileTest.png
'SilverStripe\Security\Permission':
admin:
Code: ADMIN
cmsmain:
Code: CMS_ACCESS_LeftAndMain
assetadmin:
Code: CMS_ACCESS_AssetAdmin
securityadmin:
Code: CMS_ACCESS_SecurityAdmin
'SilverStripe\Security\Group':
admins:
Title: Administrators
Permissions: '=>SilverStripe\Security\Permission.admin'
cmsusers:
Title: 'CMS Users'
Permissions: '=>SilverStripe\Security\Permission.cmsmain'
securityusers:
Title: 'Security Users'
Permissions: '=>SilverStripe\Security\Permission.securityadmin'
assetusers:
Title: 'Asset Users'
Permissions: '=>SilverStripe\Security\Permission.assetadmin'
'SilverStripe\Security\Member':
frontend:
Email: frontend@example.com
cms:
Email: cms@silverstripe.com
Groups: '=>SilverStripe\Security\Group.cmsusers'
admin:
Email: admin@silverstripe.com
Groups: '=>SilverStripe\Security\Group.admins'
assetadmin:
Email: assetadmin@silverstripe.com
Groups: '=>SilverStripe\Security\Group.assetusers'
security:
Email: security@silverstripe.com
Groups: '=>SilverStripe\Security\Group.securityusers'

View File

@ -1,11 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\FileTest;
use SilverStripe\Assets\File;
use SilverStripe\Dev\TestOnly;
class MyCustomFile extends File implements TestOnly
{
private static $table_name = 'FileTest_MyCustomFile';
}

View File

@ -1,80 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\Flysystem;
use SilverStripe\Assets\Flysystem\ProtectedAssetAdapter;
use SilverStripe\Assets\Flysystem\PublicAssetAdapter;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\File;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Core\Config\Config;
class AssetAdapterTest extends SapphireTest
{
protected $rootDir = null;
protected $originalServer = null;
public function setUp()
{
parent::setUp();
$this->rootDir = ASSETS_PATH . '/AssetAdapterTest';
Filesystem::makeFolder($this->rootDir);
Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', '/');
$this->originalServer = $_SERVER;
}
public function tearDown()
{
if ($this->rootDir) {
Filesystem::removeFolder($this->rootDir);
$this->rootDir = null;
}
if ($this->originalServer) {
$_SERVER = $this->originalServer;
$this->originalServer = null;
}
parent::tearDown();
}
public function testPublicAdapter()
{
$_SERVER['SERVER_SOFTWARE'] = 'Apache/2.2.22 (Win64) PHP/5.3.13';
$adapter = new PublicAssetAdapter($this->rootDir);
$this->assertFileExists($this->rootDir . '/.htaccess');
$this->assertFileNotExists($this->rootDir . '/web.config');
$htaccess = $adapter->read('.htaccess');
$content = $htaccess['contents'];
// Allowed extensions set
$this->assertContains('RewriteCond %{REQUEST_URI} !\\.(?i:', $content);
foreach (File::config()->allowed_extensions as $extension) {
$this->assertRegExp('/\b'.preg_quote($extension).'\b/', $content);
}
// Rewrite rules
$this->assertContains('RewriteRule .* ../framework/main.php?url=%1 [QSA]', $content);
$this->assertContains('RewriteRule error[^\\\\/]*\\.html$ - [L]', $content);
// Test flush restores invalid content
\file_put_contents($this->rootDir . '/.htaccess', '# broken content');
$adapter->flush();
$htaccess2 = $adapter->read('.htaccess');
$this->assertEquals($content, $htaccess2['contents']);
// Test URL
$this->assertEquals('/assets/AssetAdapterTest/file.jpg', $adapter->getPublicUrl('file.jpg'));
}
public function testProtectedAdapter()
{
$_SERVER['SERVER_SOFTWARE'] = 'Apache/2.2.22 (Win64) PHP/5.3.13';
$adapter = new ProtectedAssetAdapter($this->rootDir . '/.protected');
$this->assertFileExists($this->rootDir . '/.protected/.htaccess');
$this->assertFileNotExists($this->rootDir . '/.protected/web.config');
// Test url
$this->assertEquals('/assets/file.jpg', $adapter->getProtectedUrl('file.jpg'));
}
}

View File

@ -1,302 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\File;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
/**
* @author Ingo Schommer (ingo at silverstripe dot com)
*/
class FolderTest extends SapphireTest
{
protected static $fixture_file = 'FileTest.yml';
public function setUp()
{
parent::setUp();
$this->logInWithPermission('ADMIN');
Versioned::set_stage(Versioned::DRAFT);
// Set backend root to /FolderTest
TestAssetStore::activate('FolderTest');
// Set the File Name Filter replacements so files have the expected names
Config::inst()->update(
'SilverStripe\\Assets\\FileNameFilter',
'default_replacements',
array(
'/\s/' => '-', // remove whitespace
'/_/' => '-', // underscores to dashes
'/[^A-Za-z0-9+.\-]+/' => '', // remove non-ASCII chars, only allow alphanumeric plus dash and dot
'/[\-]{2,}/' => '-', // remove duplicate dashes
'/^[\.\-_]+/' => '', // Remove all leading dots, dashes or underscores
)
);
// Create a test folders for each of the fixture references
foreach (Folder::get() as $folder) {
$path = TestAssetStore::getLocalPath($folder);
Filesystem::makeFolder($path);
}
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', Folder::class);
foreach ($files as $file) {
$path = TestAssetStore::getLocalPath($file);
Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
}
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testCreateFromNameAndParentIDSetsFilename()
{
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
$newFolder = new Folder();
$newFolder->Name = 'CreateFromNameAndParentID';
$newFolder->ParentID = $folder1->ID;
$newFolder->write();
$this->assertEquals($folder1->Filename . 'CreateFromNameAndParentID/', $newFolder->Filename);
}
public function testRenamesDuplicateFolders()
{
$original = new Folder();
$original->update([
'Name' => 'folder1',
'ParentID' => 0
]);
$original->write();
$duplicate = new Folder();
$duplicate->update([
'Name' => 'folder1',
'ParentID' => 0
]);
$duplicate->write();
$original = Folder::get()->byID($original->ID);
$this->assertEquals($original->Name, 'folder1');
$this->assertEquals($original->Title, 'folder1');
$this->assertEquals($duplicate->Name, 'folder1-v2');
$this->assertEquals($duplicate->Title, 'folder1-v2');
}
public function testAllChildrenIncludesFolders()
{
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
$subfolder1 = $this->objFromFixture(Folder::class, 'folder1-subfolder1');
$file1 = $this->objFromFixture(File::class, 'file1-folder1');
$children = $folder1->allChildren();
$this->assertEquals(2, $children->Count());
$this->assertContains($subfolder1->ID, $children->column('ID'));
$this->assertContains($file1->ID, $children->column('ID'));
}
public function testFindOrMake()
{
$path = 'parent/testFindOrMake/';
$folder = Folder::find_or_make($path);
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path,
TestAssetStore::getLocalPath($folder),
'Nested path information is correctly saved to database (with trailing slash)'
);
// Folder does not exist until it contains files
$this->assertFileNotExists(
TestAssetStore::getLocalPath($folder),
'Empty folder does not have a filesystem record automatically'
);
$parentFolder = DataObject::get_one(
Folder::class,
array(
'"File"."Name"' => 'parent'
)
);
$this->assertNotNull($parentFolder);
$this->assertEquals($parentFolder->ID, $folder->ParentID);
$path = 'parent/testFindOrMake'; // no trailing slash
$folder = Folder::find_or_make($path);
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path . '/', // Slash is automatically added here
TestAssetStore::getLocalPath($folder),
'Path information is correctly saved to database (without trailing slash)'
);
$path = 'assets/'; // relative to "assets/" folder, should produce "assets/assets/"
$folder = Folder::find_or_make($path);
$this->assertEquals(
ASSETS_PATH . '/FolderTest/' . $path,
TestAssetStore::getLocalPath($folder),
'A folder named "assets/" within "assets/" is allowed'
);
}
/**
* Tests for the bug #5994 - Moving folder after executing Folder::findOrMake will not set the Filenames properly
*/
public function testFindOrMakeFolderThenMove()
{
$folder1 = $this->objFromFixture(Folder::class, 'folder1');
Folder::find_or_make($folder1->Filename);
$folder2 = $this->objFromFixture(Folder::class, 'folder2');
// Publish file1
/**
* @var File $file1
*/
$file1 = DataObject::get_by_id(File::class, $this->idFromFixture(File::class, 'file1-folder1'), false);
$file1->publishRecursive();
// set ParentID. This should cause updateFilesystem to be called on all children
$folder1->ParentID = $folder2->ID;
$folder1->write();
// Check if the file in the folder moved along
/**
* @var File $file1Draft
*/
$file1Draft = Versioned::get_by_stage(File::class, Versioned::DRAFT)->byID($file1->ID);
$this->assertFileExists(TestAssetStore::getLocalPath($file1Draft));
$this->assertEquals(
'FileTest-folder2/FileTest-folder1/File1.txt',
$file1Draft->Filename,
'The file DataObject has updated path'
);
// File should be located in new folder
$this->assertEquals(
ASSETS_PATH . '/FolderTest/.protected/FileTest-folder2/FileTest-folder1/55b443b601/File1.txt',
TestAssetStore::getLocalPath($file1Draft)
);
// Published (live) version remains in the old location
/**
* @var File $file1Live
*/
$file1Live = Versioned::get_by_stage(File::class, Versioned::LIVE)->byID($file1->ID);
$this->assertEquals(
ASSETS_PATH . '/FolderTest/FileTest-folder1/55b443b601/File1.txt',
TestAssetStore::getLocalPath($file1Live)
);
// Publishing the draft to live should move the new file to the public store
$file1Draft->publishRecursive();
$this->assertEquals(
ASSETS_PATH . '/FolderTest/FileTest-folder2/FileTest-folder1/55b443b601/File1.txt',
TestAssetStore::getLocalPath($file1Draft)
);
}
/**
* Tests for the bug #5994 - if you don't execute get_by_id prior to the rename or move, it will fail.
*/
public function testRenameFolderAndCheckTheFile()
{
// ID is prefixed in case Folder is subclassed by project/other module.
$folder1 = DataObject::get_one(
Folder::class,
array(
'"File"."ID"' => $this->idFromFixture(Folder::class, 'folder1')
)
);
$folder1->Name = 'FileTest-folder1-changed';
$folder1->write();
// Check if the file in the folder moved along
$file1 = DataObject::get_by_id(File::class, $this->idFromFixture(File::class, 'file1-folder1'), false);
$this->assertFileExists(
TestAssetStore::getLocalPath($file1)
);
$this->assertEquals(
$file1->Filename,
'FileTest-folder1-changed/File1.txt',
'The file DataObject path uses renamed folder'
);
// File should be located in new folder
$this->assertEquals(
ASSETS_PATH . '/FolderTest/.protected/FileTest-folder1-changed/55b443b601/File1.txt',
TestAssetStore::getLocalPath($file1)
);
}
/**
* URL and Link are undefined for folder dataobjects
*/
public function testLinkAndRelativeLink()
{
$folder = $this->objFromFixture(Folder::class, 'folder1');
$this->assertEmpty($folder->getURL());
$this->assertEmpty($folder->Link());
}
public function testIllegalFilenames()
{
// Test that generating a filename with invalid characters generates a correctly named folder.
$folder = Folder::find_or_make('/FolderTest/EN_US Lang');
$this->assertEquals('FolderTest/EN-US-Lang/', $folder->getFilename());
// Test repeatitions of folder
$folder2 = Folder::find_or_make('/FolderTest/EN_US Lang');
$this->assertEquals($folder->ID, $folder2->ID);
$folder3 = Folder::find_or_make('/FolderTest/EN--US_L!ang');
$this->assertEquals($folder->ID, $folder3->ID);
$folder4 = Folder::find_or_make('/FolderTest/EN-US-Lang');
$this->assertEquals($folder->ID, $folder4->ID);
}
public function testTitleTiedToName()
{
$newFolder = new Folder();
$newFolder->Name = 'TestNameCopiedToTitle';
$this->assertEquals($newFolder->Name, $newFolder->Title);
$this->assertEquals($newFolder->Title, 'TestNameCopiedToTitle');
$newFolder->Title = 'TestTitleCopiedToName';
$this->assertEquals($newFolder->Name, $newFolder->Title);
$this->assertEquals($newFolder->Title, 'TestTitleCopiedToName');
$newFolder->Name = 'TestNameWithIllegalCharactersCopiedToTitle <!BANG!>';
$this->assertEquals($newFolder->Name, $newFolder->Title);
$this->assertEquals($newFolder->Title, 'TestNameWithIllegalCharactersCopiedToTitle <!BANG!>');
$newFolder->Title = 'TestTitleWithIllegalCharactersCopiedToName <!BANG!>';
$this->assertEquals($newFolder->Name, $newFolder->Title);
$this->assertEquals($newFolder->Title, 'TestTitleWithIllegalCharactersCopiedToName <!BANG!>');
}
public function testRootFolder()
{
$root = Folder::singleton();
$this->assertEquals('/', $root->getFilename());
}
}

View File

@ -1,221 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\GDBackend;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
/**
* Tests for the {@link GD} class.
*/
class GDTest extends SapphireTest
{
public static $filenames = array(
'gif' => 'test_gif.gif',
'jpg' => 'test_jpg.jpg',
'png8' => 'test_png8.png',
'png32' => 'test_png32.png'
);
public function setUp()
{
parent::setUp();
GDBackend::flush();
}
public function tearDown()
{
GDBackend::flush();
parent::tearDown();
}
/**
* Loads all images into an associative array of GD objects.
* Optionally applies an operation to each GD
*
* @param callable $callback Action to perform on each GD
* @return array List of GD
*/
protected function applyToEachImage($callback = null)
{
$gds = array();
foreach (self::$filenames as $type => $file) {
$fullPath = realpath(__DIR__ . '/GDTest/images/' . $file);
$gd = new GDBackend();
$gd->loadFrom($fullPath);
if ($callback) {
$gd = $callback($gd);
}
$gds[$type] = $gd;
}
return $gds;
}
/**
* Takes samples from the given GD at 5 pixel increments
*
* @param GDBackend $gd The source image
* @param integer $horizontal Number of samples to take horizontally
* @param integer $vertical Number of samples to take vertically
* @return array List of colours for each sample, each given as an associative
* array with red, blue, green, and alpha components
*/
protected function sampleAreas(GDBackend $gd, $horizontal = 4, $vertical = 4)
{
$samples = array();
for ($y = 0; $y < $vertical; $y++) {
for ($x = 0; $x < $horizontal; $x++) {
$colour = imagecolorat($gd->getImageResource(), $x * 5, $y * 5);
$samples[] = imagecolorsforindex($gd->getImageResource(), $colour);
}
}
return $samples;
}
/**
* Asserts that two colour channels are equivalent within a given tolerance range
*
* @param integer $expected
* @param integer $actual
* @param integer $tolerance
*/
protected function assertColourEquals($expected, $actual, $tolerance = 0)
{
$match =
($expected + $tolerance >= $actual) &&
($expected - $tolerance <= $actual);
$this->assertTrue($match);
}
/**
* Asserts that all samples given correctly correspond to a greyscale version
* of the test image pattern
*
* @param array $samples List of 16 colour samples representing each of the 8 x 8 squares on the image pattern
* 8 x 8 squares on the image pattern
* @param int $alphaBits Depth of alpha channel in bits
* @param int $tolerance Reasonable tolerance level for colour comparison
*/
protected function assertGreyscale($samples, $alphaBits = 0, $tolerance = 0)
{
// Check that all colour samples match
foreach ($samples as $sample) {
$matches =
($sample['red'] === $sample['green']) &&
($sample['blue'] === $sample['green']);
$this->assertTrue($matches, 'Assert colour is greyscale');
if (!$matches) {
return;
}
}
// check various sample points
$this->assertColourEquals(76, $samples[0]['red'], $tolerance);
$this->assertColourEquals(149, $samples[2]['red'], $tolerance);
$this->assertColourEquals(0, $samples[8]['red'], $tolerance);
$this->assertColourEquals(127, $samples[9]['red'], $tolerance);
// check alpha of various points
switch ($alphaBits) {
case 0:
$this->assertColourEquals(0, $samples[2]['alpha'], $tolerance);
$this->assertColourEquals(0, $samples[12]['alpha'], $tolerance);
break;
case 1:
$this->assertColourEquals(0, $samples[2]['alpha'], $tolerance);
$this->assertColourEquals(127, $samples[12]['alpha'], $tolerance);
break;
default:
$this->assertColourEquals(63, $samples[2]['alpha'], $tolerance);
$this->assertColourEquals(127, $samples[12]['alpha'], $tolerance);
break;
}
}
/**
* Tests that images are correctly transformed to greyscale
*/
function testGreyscale()
{
// Apply greyscaling to each image
$images = $this->applyToEachImage(
function (GDBackend $gd) {
return $gd->greyscale();
}
);
// Test GIF (256 colour, transparency)
$samplesGIF = $this->sampleAreas($images['gif']);
$this->assertGreyscale($samplesGIF, 1);
// Test JPG
$samplesJPG = $this->sampleAreas($images['jpg']);
$this->assertGreyscale($samplesJPG, 0, 4);
// Test PNG 8 (indexed with alpha transparency)
$samplesPNG8 = $this->sampleAreas($images['png8']);
$this->assertGreyscale($samplesPNG8, 8, 4);
// Test PNG 32 (full alpha transparency)
$samplesPNG32 = $this->sampleAreas($images['png32']);
$this->assertGreyscale($samplesPNG32, 8);
}
/**
* Tests that GD doesn't attempt to load images when they're deemed unavailable
*
* @return void
*/
public function testImageSkippedWhenUnavailable()
{
$fullPath = realpath(__DIR__ . '/GDTest/images/test_jpg.jpg');
$gd = new GDTest\ImageUnavailable();
$gd->loadFrom($fullPath);
/* Ensure no image resource is created if the image is unavailable */
$this->assertNull($gd->getImageResource());
}
/**
* Tests the integrity of the manipulation cache when an error occurs
*/
public function testCacheIntegrity()
{
$fullPath = realpath(__DIR__ . '/GDTest/images/nonimagedata.jpg');
// Load invalid file
$gd = new GDBackend();
$gd->loadFrom($fullPath);
// Cache should refer to this file
$cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations');
$key = sha1(implode('|', array($fullPath, filemtime($fullPath))));
$data = $cache->get($key);
$this->assertEquals('1', $data);
}
/**
* Test that GD::failedResample() returns true for the current image
* manipulation only if it previously failed
*
* @return void
*/
public function testFailedResample()
{
$fullPath = realpath(__DIR__ . '/GDTest/images/nonimagedata.jpg');
$fullPath2 = realpath(__DIR__ . '/GDTest/images/test_gif.gif');
// Load invalid file
$gd = new GDBackend();
$gd->loadFrom($fullPath);
// Cache should refre to this file
$this->assertTrue($gd->failedResample($fullPath, filemtime($fullPath)));
$this->assertFalse($gd->failedResample($fullPath2, filemtime($fullPath2)));
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\GDTest;
use SilverStripe\Assets\GDBackend;
use SilverStripe\Dev\TestOnly;
class ImageUnavailable extends GDBackend implements TestOnly
{
public function failedResample($arg = null)
{
return true;
}
}

View File

@ -1 +0,0 @@
No this is not an image file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

View File

@ -1,224 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Storage\ProtectedFileController;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Assets\File;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
class ProtectedFileControllerTest extends FunctionalTest
{
protected static $fixture_file = 'FileTest.yml';
public function setUp()
{
parent::setUp();
// Set backend root to /ImageTest
TestAssetStore::activate('ProtectedFileControllerTest');
// Create a test folders for each of the fixture references
foreach (Folder::get() as $folder) {
/**
* @var Folder $folder
*/
$filePath = TestAssetStore::getLocalPath($folder);
Filesystem::makeFolder($filePath);
}
// Create a test files for each of the fixture references
foreach (File::get()->exclude('ClassName', Folder::class) as $file) {
/**
* @var File $file
*/
$path = TestAssetStore::getLocalPath($file);
Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
// Create variant for each file
$this->getAssetStore()->setFromString(
str_repeat('y', 100),
$file->Filename,
$file->Hash,
'variant'
);
}
}
/**
* @dataProvider getFilenames
*/
public function testIsValidFilename($name, $isValid)
{
$controller = new ProtectedFileController();
$this->assertEquals(
$isValid,
$controller->isValidFilename($name),
"Assert filename \"$name\" is " . $isValid ? "valid" : "invalid"
);
}
public function getFilenames()
{
return array(
// Valid names
array('name.jpg', true),
array('parent/name.jpg', true),
array('parent/name', true),
array('parent\name.jpg', true),
array('parent\name', true),
array('name', true),
// Invalid names
array('.invalid/name.jpg', false),
array('.invalid\name.jpg', false),
array('.htaccess', false),
array('test/.htaccess.jpg', false),
array('name/.jpg', false),
array('test\.htaccess.jpg', false),
array('name\.jpg', false)
);
}
/**
* Test that certain requests are denied
*/
public function testInvalidRequest()
{
$result = $this->get('assets/.protected/file.jpg');
$this->assertResponseEquals(400, null, $result);
}
/**
* Test that invalid files generate 404 response
*/
public function testFileNotFound()
{
$result = $this->get('assets/missing.jpg');
$this->assertResponseEquals(404, null, $result);
}
/**
* Check public access to assets is available at the appropriate time
*/
public function testAccessControl()
{
$expectedContent = str_repeat('x', 1000000);
$variantContent = str_repeat('y', 100);
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(200, $expectedContent, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(200, $variantContent, $result);
// Make this file protected
$this->getAssetStore()->protect(
'FileTest.txt',
'55b443b60176235ef09801153cca4e6da7494a0c'
);
// Should now return explicitly denied errors
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(403, null, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(403, null, $result);
// Other assets remain available
$result = $this->get('assets/55b443b601/FileTest.pdf');
$this->assertResponseEquals(200, $expectedContent, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.pdf');
$this->assertResponseEquals(200, $variantContent, $result);
// granting access will allow access
$this->getAssetStore()->grant(
'FileTest.txt',
'55b443b60176235ef09801153cca4e6da7494a0c'
);
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(200, $expectedContent, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(200, $variantContent, $result);
// Revoking access will remove access again
$this->getAssetStore()->revoke(
'FileTest.txt',
'55b443b60176235ef09801153cca4e6da7494a0c'
);
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(403, null, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(403, null, $result);
// Moving file back to public store restores access
$this->getAssetStore()->publish(
'FileTest.txt',
'55b443b60176235ef09801153cca4e6da7494a0c'
);
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(200, $expectedContent, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(200, $variantContent, $result);
// Deleting the file will make the response 404
$this->getAssetStore()->delete(
'FileTest.txt',
'55b443b60176235ef09801153cca4e6da7494a0c'
);
$result = $this->get('assets/55b443b601/FileTest.txt');
$this->assertResponseEquals(404, null, $result);
$result = $this->get('assets/55b443b601/FileTest__variant.txt');
$this->assertResponseEquals(404, null, $result);
}
/**
* Test that access to folders is not permitted
*/
public function testFolders()
{
$result = $this->get('assets/55b443b601');
$this->assertResponseEquals(403, null, $result);
$result = $this->get('assets/FileTest-subfolder');
$this->assertResponseEquals(403, null, $result);
// Flysystem reports root folder as not present
$result = $this->get('assets');
$this->assertResponseEquals(404, null, $result);
}
/**
* @return AssetStore
*/
protected function getAssetStore()
{
return Injector::inst()->get('AssetStore');
}
/**
* Assert that a response matches the given parameters
*
* @param int $code HTTP code
* @param string $body Body expected for 200 responses
* @param HTTPResponse $response
*/
protected function assertResponseEquals($code, $body, HTTPResponse $response)
{
$this->assertEquals($code, $response->getStatusCode());
if ($code === 200) {
$this->assertFalse($response->isError());
$this->assertEquals($body, $response->getBody());
$this->assertEquals('text/plain', $response->getHeader('Content-Type'));
} else {
$this->assertTrue($response->isError());
}
}
}

View File

@ -1,523 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\Storage;
use Exception;
use SilverStripe\Assets\Flysystem\FlysystemAssetStore;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
class AssetStoreTest extends SapphireTest
{
/**
* @skipUpgrade
*/
public function setUp()
{
parent::setUp();
// Set backend and base url
TestAssetStore::activate('AssetStoreTest');
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
/**
* @return TestAssetStore
*/
protected function getBackend()
{
return Injector::inst()->get('AssetStore');
}
/**
* Test different storage methods
*/
public function testStorageMethods()
{
$backend = $this->getBackend();
// Test setFromContent
$puppies1 = 'puppies';
$puppies1Tuple = $backend->setFromString($puppies1, 'pets/my-puppy.txt');
$this->assertEquals(
array(
'Hash' => '2a17a9cb4be918774e73ba83bd1c1e7d000fdd53',
'Filename' => 'pets/my-puppy.txt',
'Variant' => '',
),
$puppies1Tuple
);
// Test setFromStream (seekable)
$fish1 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-high-quality.jpg');
$fish1Stream = fopen($fish1, 'r');
$fish1Tuple = $backend->setFromStream($fish1Stream, 'parent/awesome-fish.jpg');
fclose($fish1Stream);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'parent/awesome-fish.jpg',
'Variant' => '',
),
$fish1Tuple
);
// Test with non-seekable streams
TestAssetStore::$seekable_override = false;
$fish2 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-low-quality.jpg');
$fish2Stream = fopen($fish2, 'r');
$fish2Tuple = $backend->setFromStream($fish2Stream, 'parent/mediocre-fish.jpg');
fclose($fish2Stream);
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
'Filename' => 'parent/mediocre-fish.jpg',
'Variant' => '',
),
$fish2Tuple
);
TestAssetStore::$seekable_override = null;
}
/**
* Test that the backend correctly resolves conflicts
*/
public function testConflictResolution()
{
$backend = $this->getBackend();
// Put a file in
$fish1 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-high-quality.jpg');
$this->assertFileExists($fish1);
$fish1Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg');
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish.jpg',
'Variant' => '',
),
$fish1Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Filename'], $fish1Tuple['Hash'])
);
// Write a different file with same name. Should not detect duplicates since sha are different
$fish2 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-low-quality.jpg');
try {
$fish2Tuple = $backend->setFromLocalFile(
$fish2,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_EXCEPTION)
);
} catch (Exception $ex) {
$this->fail('Writing file with different sha to same location failed with exception');
return;
}
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
'Filename' => 'directory/lovely-fish.jpg',
'Variant' => '',
),
$fish2Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/33be1b95cb/lovely-fish.jpg',
$backend->getAsURL($fish2Tuple['Filename'], $fish2Tuple['Hash'])
);
// Write original file back with rename
$this->assertFileExists($fish1);
$fish3Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_RENAME)
);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish-v2.jpg',
'Variant' => '',
),
$fish3Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/a870de278b/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Filename'], $fish3Tuple['Hash'])
);
// Write another file should increment to -v3
$fish4Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish-v2.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_RENAME)
);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish-v3.jpg',
'Variant' => '',
),
$fish4Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/a870de278b/lovely-fish-v3.jpg',
$backend->getAsURL($fish4Tuple['Filename'], $fish4Tuple['Hash'])
);
// Test conflict use existing file
$fish5Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_USE_EXISTING)
);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish.jpg',
'Variant' => '',
),
$fish5Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish5Tuple['Filename'], $fish5Tuple['Hash'])
);
// Test conflict use existing file
$fish6Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_OVERWRITE)
);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish.jpg',
'Variant' => '',
),
$fish6Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fish6Tuple['Filename'], $fish6Tuple['Hash'])
);
}
/**
* Test that flysystem can regenerate the original filename from fileID
*/
public function testGetOriginalFilename()
{
$store = new TestAssetStore();
$this->assertEquals(
'directory/lovely-fish.jpg',
$store->getOriginalFilename('directory/a870de278b/lovely-fish.jpg')
);
$this->assertEquals(
'directory/lovely-fish.jpg',
$store->getOriginalFilename('directory/a870de278b/lovely-fish__variant.jpg')
);
$this->assertEquals(
'directory/lovely_fish.jpg',
$store->getOriginalFilename('directory/a870de278b/lovely_fish__vari_ant.jpg')
);
$this->assertEquals(
'directory/lovely_fish.jpg',
$store->getOriginalFilename('directory/a870de278b/lovely_fish.jpg')
);
$this->assertEquals(
'lovely-fish.jpg',
$store->getOriginalFilename('a870de278b/lovely-fish.jpg')
);
$this->assertEquals(
'lovely-fish.jpg',
$store->getOriginalFilename('a870de278b/lovely-fish__variant.jpg')
);
$this->assertEquals(
'lovely_fish.jpg',
$store->getOriginalFilename('a870de278b/lovely_fish__vari__ant.jpg')
);
$this->assertEquals(
'lovely_fish.jpg',
$store->getOriginalFilename('a870de278b/lovely_fish.jpg')
);
}
/**
* Test internal file Id generation
*/
public function testGetFileID()
{
$store = new TestAssetStore();
$this->assertEquals(
'directory/2a17a9cb4b/file.jpg',
$store->getFileID('directory/file.jpg', sha1('puppies'))
);
$this->assertEquals(
'2a17a9cb4b/file.jpg',
$store->getFileID('file.jpg', sha1('puppies'))
);
$this->assertEquals(
'dir_ectory/2a17a9cb4b/fil_e.jpg',
$store->getFileID('dir__ectory/fil__e.jpg', sha1('puppies'))
);
$this->assertEquals(
'directory/2a17a9cb4b/file_variant.jpg',
$store->getFileID('directory/file__variant.jpg', sha1('puppies'), null)
);
$this->assertEquals(
'directory/2a17a9cb4b/file__variant.jpg',
$store->getFileID('directory/file.jpg', sha1('puppies'), 'variant')
);
$this->assertEquals(
'2a17a9cb4b/file__var__iant.jpg',
$store->getFileID('file.jpg', sha1('puppies'), 'var__iant')
);
}
public function testGetMetadata()
{
$backend = $this->getBackend();
// jpg
$fish = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-high-quality.jpg');
$fishTuple = $backend->setFromLocalFile($fish, 'parent/awesome-fish.jpg');
$this->assertEquals(
'image/jpeg',
$backend->getMimeType($fishTuple['Filename'], $fishTuple['Hash'])
);
$fishMeta = $backend->getMetadata($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertEquals(151889, $fishMeta['size']);
$this->assertEquals('file', $fishMeta['type']);
$this->assertNotEmpty($fishMeta['timestamp']);
// text
$puppies = 'puppies';
$puppiesTuple = $backend->setFromString($puppies, 'pets/my-puppy.txt');
$this->assertEquals(
'text/plain',
$backend->getMimeType($puppiesTuple['Filename'], $puppiesTuple['Hash'])
);
$puppiesMeta = $backend->getMetadata($puppiesTuple['Filename'], $puppiesTuple['Hash']);
$this->assertEquals(7, $puppiesMeta['size']);
$this->assertEquals('file', $puppiesMeta['type']);
$this->assertNotEmpty($puppiesMeta['timestamp']);
}
/**
* Test that legacy filenames work as expected
*/
public function testLegacyFilenames()
{
Config::modify()->set(FlysystemAssetStore::class, 'legacy_filenames', true);
$backend = $this->getBackend();
// Put a file in
$fish1 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-high-quality.jpg');
$this->assertFileExists($fish1);
$fish1Tuple = $backend->setFromLocalFile($fish1, 'directory/lovely-fish.jpg');
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish.jpg',
'Variant' => '',
),
$fish1Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/lovely-fish.jpg',
$backend->getAsURL($fish1Tuple['Filename'], $fish1Tuple['Hash'])
);
// Write a different file with same name.
// Since we are using legacy filenames, this should generate a new filename
$fish2 = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-low-quality.jpg');
try {
$backend->setFromLocalFile(
$fish2,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_EXCEPTION)
);
$this->fail('Writing file with different sha to same location should throw exception');
return;
} catch (Exception $ex) {
// Success
}
// Re-attempt this file write with conflict_rename
$fish3Tuple = $backend->setFromLocalFile(
$fish2,
'directory/lovely-fish.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_RENAME)
);
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
'Filename' => 'directory/lovely-fish-v2.jpg',
'Variant' => '',
),
$fish3Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish3Tuple['Filename'], $fish3Tuple['Hash'])
);
// Write back original file, but with CONFLICT_EXISTING. The file should not change
$fish4Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish-v2.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_USE_EXISTING)
);
$this->assertEquals(
array(
'Hash' => '33be1b95cba0358fe54e8b13532162d52f97421c',
'Filename' => 'directory/lovely-fish-v2.jpg',
'Variant' => '',
),
$fish4Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish4Tuple['Filename'], $fish4Tuple['Hash'])
);
// Write back original file with CONFLICT_OVERWRITE. The file sha should now be updated
$fish5Tuple = $backend->setFromLocalFile(
$fish1,
'directory/lovely-fish-v2.jpg',
null,
null,
array('conflict' => AssetStore::CONFLICT_OVERWRITE)
);
$this->assertEquals(
array(
'Hash' => 'a870de278b475cb75f5d9f451439b2d378e13af1',
'Filename' => 'directory/lovely-fish-v2.jpg',
'Variant' => '',
),
$fish5Tuple
);
$this->assertEquals(
'/assets/AssetStoreTest/directory/lovely-fish-v2.jpg',
$backend->getAsURL($fish5Tuple['Filename'], $fish5Tuple['Hash'])
);
}
/**
* Test default conflict resolution
*/
public function testDefaultConflictResolution()
{
$store = $this->getBackend();
// Disable legacy filenames
Config::modify()->set(FlysystemAssetStore::class, 'legacy_filenames', false);
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution(null));
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution('somevariant'));
// Enable legacy filenames
Config::modify()->set(FlysystemAssetStore::class, 'legacy_filenames', true);
$this->assertEquals(AssetStore::CONFLICT_RENAME, $store->getDefaultConflictResolution(null));
$this->assertEquals(AssetStore::CONFLICT_OVERWRITE, $store->getDefaultConflictResolution('somevariant'));
}
/**
* Test protect / publish mechanisms
*/
public function testProtect()
{
$backend = $this->getBackend();
$fish = realpath(__DIR__ . '/../../ORM/ImageTest/test-image-high-quality.jpg');
$fishTuple = $backend->setFromLocalFile($fish, 'parent/lovely-fish.jpg');
$fishVariantTuple = $backend->setFromLocalFile($fish, $fishTuple['Filename'], $fishTuple['Hash'], 'copy');
// Test public file storage
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish.jpg');
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish__copy.jpg');
$this->assertEquals(
AssetStore::VISIBILITY_PUBLIC,
$backend->getVisibility($fishTuple['Filename'], $fishTuple['Hash'])
);
$this->assertEquals(
'/assets/AssetStoreTest/parent/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fishTuple['Filename'], $fishTuple['Hash'])
);
$this->assertEquals(
'/assets/AssetStoreTest/parent/a870de278b/lovely-fish__copy.jpg',
$backend->getAsURL($fishVariantTuple['Filename'], $fishVariantTuple['Hash'], $fishVariantTuple['Variant'])
);
// Test access rights to public files cannot be revoked
$backend->revoke($fishTuple['Filename'], $fishTuple['Hash']); // can't revoke public assets
$this->assertTrue($backend->canView($fishTuple['Filename'], $fishTuple['Hash']));
// Test protected file storage
$backend->protect($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertFileNotExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish.jpg');
$this->assertFileNotExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish__copy.jpg');
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/.protected/parent/a870de278b/lovely-fish.jpg');
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/.protected/parent/a870de278b/lovely-fish__copy.jpg');
$this->assertEquals(
AssetStore::VISIBILITY_PROTECTED,
$backend->getVisibility($fishTuple['Filename'], $fishTuple['Hash'])
);
// Test access rights
$backend->revoke($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertFalse($backend->canView($fishTuple['Filename'], $fishTuple['Hash']));
$backend->grant($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertTrue($backend->canView($fishTuple['Filename'], $fishTuple['Hash']));
// Protected urls should go through asset routing mechanism
$this->assertEquals(
'/assets/parent/a870de278b/lovely-fish.jpg',
$backend->getAsURL($fishTuple['Filename'], $fishTuple['Hash'])
);
$this->assertEquals(
'/assets/parent/a870de278b/lovely-fish__copy.jpg',
$backend->getAsURL($fishVariantTuple['Filename'], $fishVariantTuple['Hash'], $fishVariantTuple['Variant'])
);
// Publish reverts visibility
$backend->publish($fishTuple['Filename'], $fishTuple['Hash']);
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish.jpg');
$this->assertFileExists(ASSETS_PATH . '/AssetStoreTest/parent/a870de278b/lovely-fish__copy.jpg');
$this->assertFileNotExists(ASSETS_PATH . '/AssetStoreTest/.protected/parent/a870de278b/lovely-fish.jpg');
$this->assertFileNotExists(ASSETS_PATH . '/AssetStoreTest/.protected/parent/a870de278b/lovely-fish__copy.jpg');
$this->assertEquals(
AssetStore::VISIBILITY_PUBLIC,
$backend->getVisibility($fishTuple['Filename'], $fishTuple['Hash'])
);
}
}

View File

@ -1,183 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\Storage\AssetStoreTest;
use League\Flysystem\Adapter\Local;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Filesystem;
use SilverStripe\Assets\Filesystem as SSFilesystem;
use SilverStripe\Assets\Flysystem\FlysystemAssetStore;
use SilverStripe\Assets\Flysystem\ProtectedAssetAdapter;
use SilverStripe\Assets\Flysystem\PublicAssetAdapter;
use SilverStripe\Assets\Storage\AssetContainer;
use SilverStripe\Assets\Flysystem\GeneratedAssetHandler;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Folder;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\View\Requirements;
/**
* Allows you to mock a backend store in a custom directory beneath assets.
*/
class TestAssetStore extends FlysystemAssetStore
{
/**
* Enable disclosure of secure assets
*
* @config
* @var int
*/
private static $denied_response_code = 403;
/**
* Set to true|false to override all isSeekableStream calls
*
* @var null|bool
*/
public static $seekable_override = null;
/**
* Base dir of current file
*
* @var string
*/
public static $basedir = null;
/**
* Set this store as the new asset backend
*
* @param string $basedir Basedir to store assets, which will be placed beneath 'assets' folder
*/
public static function activate($basedir)
{
// Assign this as the new store
$publicAdapter = new PublicAssetAdapter(ASSETS_PATH . '/' . $basedir);
$publicFilesystem = new Filesystem(
$publicAdapter,
[
'visibility' => AdapterInterface::VISIBILITY_PUBLIC
]
);
$protectedAdapter = new ProtectedAssetAdapter(ASSETS_PATH . '/' . $basedir . '/.protected');
$protectedFilesystem = new Filesystem(
$protectedAdapter,
[
'visibility' => AdapterInterface::VISIBILITY_PRIVATE
]
);
$backend = new TestAssetStore();
$backend->setPublicFilesystem($publicFilesystem);
$backend->setProtectedFilesystem($protectedFilesystem);
Injector::inst()->registerService($backend, 'AssetStore');
// Assign flysystem backend to generated asset handler at the same time
$generated = new GeneratedAssetHandler();
$generated->setFilesystem($publicFilesystem);
Injector::inst()->registerService($generated, 'GeneratedAssetHandler');
Requirements::backend()->setAssetHandler($generated);
// Disable legacy and set defaults
FlysystemAssetStore::config()->set('legacy_filenames', false);
Director::config()->set('alternate_base_url', '/');
DBFile::config()->set('force_resample', false);
File::config()->set('force_resample', false);
self::reset();
self::$basedir = $basedir;
// Ensure basedir exists
SSFilesystem::makeFolder(self::base_path());
}
/**
* Get absolute path to basedir
*
* @return string
*/
public static function base_path()
{
if (!self::$basedir) {
return null;
}
return ASSETS_PATH . '/' . self::$basedir;
}
/**
* Reset defaults for this store
*/
public static function reset()
{
// Remove all files in this store
if (self::$basedir) {
$path = self::base_path();
if (file_exists($path)) {
SSFilesystem::removeFolder($path);
}
}
self::$seekable_override = null;
self::$basedir = null;
}
/**
* Helper method to get local filesystem path for this file
*
* @param AssetContainer $asset
* @return string
*/
public static function getLocalPath(AssetContainer $asset)
{
if ($asset instanceof Folder) {
return self::base_path() . '/' . $asset->getFilename();
}
if ($asset instanceof File) {
$asset = $asset->File;
}
// Extract filesystem used to store this object
/** @var TestAssetStore $assetStore */
$assetStore = Injector::inst()->get('AssetStore');
$fileID = $assetStore->getFileID($asset->Filename, $asset->Hash, $asset->Variant);
$filesystem = $assetStore->getProtectedFilesystem();
if (!$filesystem->has($fileID)) {
$filesystem = $assetStore->getPublicFilesystem();
}
/** @var Local $adapter */
$adapter = $filesystem->getAdapter();
return $adapter->applyPathPrefix($fileID);
}
public function cleanFilename($filename)
{
return parent::cleanFilename($filename);
}
public function getFileID($filename, $hash, $variant = null)
{
return parent::getFileID($filename, $hash, $variant);
}
public function getOriginalFilename($fileID)
{
return parent::getOriginalFilename($fileID);
}
public function removeVariant($fileID)
{
return parent::removeVariant($fileID);
}
public function getDefaultConflictResolution($variant)
{
return parent::getDefaultConflictResolution($variant);
}
protected function isSeekableStream($stream)
{
if (isset(self::$seekable_override)) {
return self::$seekable_override;
}
return parent::isSeekableStream($stream);
}
}

View File

@ -1,135 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\Storage;
use SilverStripe\Assets\Storage\DefaultAssetNameGenerator;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
/**
* covers {@see DefaultAssetNameGenerator}
*/
class DefaultAssetNameGeneratorTest extends SapphireTest
{
/**
* Test non-prefix behaviour
*/
public function testWithoutPrefix()
{
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '');
$generator = new DefaultAssetNameGenerator('folder/MyFile-001.jpg');
$suggestions = iterator_to_array($generator);
// Expect 100 suggestions
$this->assertEquals(100, count($suggestions));
// First item is always the same as input
$this->assertEquals('folder/MyFile-001.jpg', $suggestions[0]);
// Check that padding is respected
$this->assertEquals('folder/MyFile-002.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-003.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-004.jpg', $suggestions[3]);
$this->assertEquals('folder/MyFile-021.jpg', $suggestions[20]);
$this->assertEquals('folder/MyFile-099.jpg', $suggestions[98]);
// Last item should be some semi-random string, not in the same numeric sequence
$this->assertNotEquals('folder/MyFile-0100.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-100.jpg', $suggestions[99]);
// Test with a value starting above 1
$generator = new DefaultAssetNameGenerator('folder/MyFile-024.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-024.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-025.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-026.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-048.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-122.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-0123.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-123.jpg', $suggestions[99]); // Last suggestion is semi-random
// Test without numeric value
$generator = new DefaultAssetNameGenerator('folder/MyFile.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile3.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile25.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile100.jpg', $suggestions[99]); // Last suggestion is semi-random
}
/**
* Test with default -v prefix
*/
public function testWithDefaultPrefix()
{
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '-v');
// Test with item that doesn't contain the prefix
$generator = new DefaultAssetNameGenerator('folder/MyFile-001.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-001.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-001-v2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-001-v4.jpg', $suggestions[3]);
$this->assertEquals('folder/MyFile-001-v21.jpg', $suggestions[20]);
$this->assertEquals('folder/MyFile-001-v99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-001-v100.jpg', $suggestions[99]); // Last suggestion is semi-random
// Test with item that contains prefix
$generator = new DefaultAssetNameGenerator('folder/MyFile-v24.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile-v24.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-v25.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-v26.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-v48.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-v122.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-v123.jpg', $suggestions[99]);
$this->assertNotEquals('folder/MyFile-123.jpg', $suggestions[99]);
// Test without numeric value
$generator = new DefaultAssetNameGenerator('folder/MyFile.jpg');
$suggestions = iterator_to_array($generator);
$this->assertEquals(100, count($suggestions));
$this->assertEquals('folder/MyFile.jpg', $suggestions[0]);
$this->assertEquals('folder/MyFile-v2.jpg', $suggestions[1]);
$this->assertEquals('folder/MyFile-v3.jpg', $suggestions[2]);
$this->assertEquals('folder/MyFile-v25.jpg', $suggestions[24]);
$this->assertEquals('folder/MyFile-v99.jpg', $suggestions[98]);
$this->assertNotEquals('folder/MyFile-v100.jpg', $suggestions[99]);
}
public function testFolderWithoutDefaultPrefix()
{
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '');
$generator = new DefaultAssetNameGenerator('folder/subfolder');
$suggestions = iterator_to_array($generator);
// Expect 100 suggestions
$this->assertEquals(100, count($suggestions));
// First item is always the same as input
$this->assertEquals('folder/subfolder', $suggestions[0]);
$this->assertEquals('folder/subfolder2', $suggestions[1]);
}
public function testFolderWithDefaultPrefix()
{
Config::inst()->update(DefaultAssetNameGenerator::class, 'version_prefix', '-v');
$generator = new DefaultAssetNameGenerator('folder/subfolder');
$suggestions = iterator_to_array($generator);
// Expect 100 suggestions
$this->assertEquals(100, count($suggestions));
// First item is always the same as input
$this->assertEquals('folder/subfolder', $suggestions[0]);
$this->assertEquals('folder/subfolder-v2', $suggestions[1]);
}
}

View File

@ -1,850 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Upload;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Versioning\Versioned;
class UploadTest extends SapphireTest
{
/**
* {@inheritDoc}
* @var bool
*/
protected $usesDatabase = true;
/**
* The temporary file path used for upload tests
* @var string
*/
protected $tmpFilePath;
/**
* {@inheritDoc}
*/
public function setUp()
{
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
TestAssetStore::activate('UploadTest');
}
/**
* {@inheritDoc}
*/
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
if (file_exists($this->tmpFilePath)) {
unlink($this->tmpFilePath);
}
}
public function testUpload()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.txt';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
// test upload into default folder
$u1 = new Upload();
$u1->setValidator($v);
$u1->loadIntoFile($tmpFile);
$file1 = $u1->getFile();
$this->assertEquals(
'Uploads/UploadTest-testUpload.txt',
$file1->getFilename()
);
$this->assertEquals(
BASE_PATH . '/assets/UploadTest/.protected/Uploads/315ae4c3d4/UploadTest-testUpload.txt',
TestAssetStore::getLocalPath($file1)
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file1),
'File upload to standard directory in /assets'
);
// test upload into custom folder
$customFolder = 'UploadTest-testUpload';
$u2 = new Upload();
$u2->loadIntoFile($tmpFile, null, $customFolder);
$file2 = $u2->getFile();
$this->assertEquals(
'UploadTest-testUpload/UploadTest-testUpload.txt',
$file2->getFilename()
);
$this->assertEquals(
BASE_PATH . '/assets/UploadTest/.protected/UploadTest-testUpload/315ae4c3d4/UploadTest-testUpload.txt',
TestAssetStore::getLocalPath($file2)
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file2),
'File upload to custom directory in /assets'
);
}
public function testAllowedFilesize()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.txt';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
// test upload into default folder
$u1 = new Upload();
$v = new UploadTest\Validator();
$v->setAllowedMaxFileSize(array('txt' => 10));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[document]' => 10));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('txt' => 200000));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
// check max file size set by app category
$tmpFileName = 'UploadTest-testUpload.jpg';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
file_put_contents($this->tmpFilePath, $tmpFileContent . $tmpFileContent);
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'image/jpeg',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'jpg',
'error' => UPLOAD_ERR_OK,
);
$v->setAllowedMaxFileSize(array('[image]' => '40k'));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
$v->setAllowedMaxFileSize(array('[image]' => '1k'));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[image]' => 1000));
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
}
public function testPHPUploadErrors()
{
$configMaxFileSizes = ['*' => '1k'];
Config::inst()->update(
'SilverStripe\\Assets\\Upload_Validator',
'default_max_file_size',
$configMaxFileSizes
);
// create tmp file
$tmpFileName = 'myfile.jpg';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent(100);
file_put_contents($this->tmpFilePath, $tmpFileContent);
// Build file
$upload = new Upload();
$tmpFile = array(
'name' => $tmpFileName,
'type' => '',
'tmp_name' => $this->tmpFilePath,
'size' => filesize($this->tmpFilePath),
'error' => UPLOAD_ERR_OK,
);
// Test ok
$this->assertTrue($upload->validate($tmpFile));
// Test zero size file
$upload->clearErrors();
$tmpFile['size'] = 0;
$this->assertFalse($upload->validate($tmpFile));
$this->assertContains(
_t('File.NOFILESIZE', 'Filesize is zero bytes.'),
$upload->getErrors()
);
// Test file too large
$upload->clearErrors();
$tmpFile['error'] = UPLOAD_ERR_INI_SIZE;
$this->assertFalse($upload->validate($tmpFile));
$this->assertContains(
_t(
'File.TOOLARGE',
'Filesize is too large, maximum {size} allowed',
'Argument 1: Filesize (e.g. 1MB)',
array('size' => '1 KB')
),
$upload->getErrors()
);
// Test form size
$upload->clearErrors();
$tmpFile['error'] = UPLOAD_ERR_FORM_SIZE;
$this->assertFalse($upload->validate($tmpFile));
$this->assertContains(
_t(
'File.TOOLARGE',
'Filesize is too large, maximum {size} allowed',
'Argument 1: Filesize (e.g. 1MB)',
array('size' => '1 KB')
),
$upload->getErrors()
);
// Test no file
$upload->clearErrors();
$tmpFile['error'] = UPLOAD_ERR_NO_FILE;
$this->assertFalse($upload->validate($tmpFile));
$this->assertContains(
_t('File.NOVALIDUPLOAD', 'File is not a valid upload'),
$upload->getErrors()
);
}
public function testGetAllowedMaxFileSize()
{
Config::nest();
// Check the max file size uses the config values
$configMaxFileSizes = array(
'[image]' => '1k',
'txt' => 1000
);
Config::inst()->update('SilverStripe\\Assets\\Upload_Validator', 'default_max_file_size', $configMaxFileSizes);
$v = new UploadTest\Validator();
$retrievedSize = $v->getAllowedMaxFileSize('[image]');
$this->assertEquals(
1024,
$retrievedSize,
'Max file size check on default values failed (config category set check)'
);
$retrievedSize = $v->getAllowedMaxFileSize('txt');
$this->assertEquals(
1000,
$retrievedSize,
'Max file size check on default values failed (config extension set check)'
);
// Check instance values for max file size
$maxFileSizes = array(
'[document]' => 2000,
'txt' => '4k'
);
$v = new UploadTest\Validator();
$v->setAllowedMaxFileSize($maxFileSizes);
$retrievedSize = $v->getAllowedMaxFileSize('[document]');
$this->assertEquals(
2000,
$retrievedSize,
'Max file size check on instance values failed (instance category set check)'
);
// Check that the instance values overwrote the default values
// ie. The max file size will not exist for [image]
$retrievedSize = $v->getAllowedMaxFileSize('[image]');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (config overridden check)');
// Check a category that has not been set before
$retrievedSize = $v->getAllowedMaxFileSize('[archive]');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (category not set check)');
// Check a file extension that has not been set before
$retrievedSize = $v->getAllowedMaxFileSize('mp3');
$this->assertFalse($retrievedSize, 'Max file size check on instance values failed (extension not set check)');
$retrievedSize = $v->getAllowedMaxFileSize('txt');
$this->assertEquals(
4096,
$retrievedSize,
'Max file size check on instance values failed (instance extension set check)'
);
// Check a wildcard max file size against a file with an extension
$v = new UploadTest\Validator();
$v->setAllowedMaxFileSize(2000);
$retrievedSize = $v->getAllowedMaxFileSize('.jpg');
$this->assertEquals(
2000,
$retrievedSize,
'Max file size check on instance values failed (wildcard max file size)'
);
Config::unnest();
}
public function testAllowedSizeOnFileWithNoExtension()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => '',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedMaxFileSize(array('' => 10));
// test upload into default folder
$u1 = new Upload();
$u1->setValidator($v);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
}
public function testUploadDoesNotAllowUnknownExtension()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.php';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'php',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedExtensions(array('txt'));
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$result = $u->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because extension was not accepted');
}
public function testUploadAcceptsAllowedExtension()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.txt';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedExtensions(array('txt'));
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertFileExists(
TestAssetStore::getLocalPath($file),
'File upload to custom directory in /assets'
);
}
public function testUploadDeniesNoExtensionFilesIfNoEmptyStringSetForValidatorExtensions()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => '',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedExtensions(array('txt'));
// test upload into default folder
$u = new Upload();
$result = $u->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because extension was not accepted');
$this->assertEquals(1, count($u->getErrors()), 'There is a single error of the file extension');
}
public function testUploadTarGzFileTwiceAppendsNumber()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.tar.gz';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'tar.gz',
'error' => UPLOAD_ERR_OK,
);
// test upload into default folder
$u = new Upload();
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.tar.gz',
$file->Name,
'File has a name without a number because it\'s not a duplicate'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file),
'File exists'
);
$u = new Upload();
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2.tar.gz',
$file2->Name,
'File receives a number attached to the end before the extension'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file2),
'File exists'
);
$this->assertGreaterThan(
$file->ID,
$file2->ID,
'File database record is not the same'
);
$u = new Upload();
$u->loadIntoFile($tmpFile);
$file3 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v3.tar.gz',
$file3->Name,
'File receives a number attached to the end before the extension'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file3),
'File exists'
);
$this->assertGreaterThan(
$file2->ID,
$file3->ID,
'File database record is not the same'
);
}
public function testUploadFileWithNoExtensionTwiceAppendsNumber()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedExtensions(array(''));
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload',
$file->Name,
'File is uploaded without extension'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file),
'File exists'
);
$u = new Upload();
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2',
$file2->Name,
'File receives a number attached to the end'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file2),
'File exists'
);
$this->assertGreaterThan(
$file->ID,
$file2->ID,
'File database record is not the same'
);
}
public function testReplaceFile()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
$v->setAllowedExtensions(array(''));
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload',
$file->Name,
'File is uploaded without extension'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file),
'File exists'
);
$u = new Upload();
$u->setValidator($v);
$u->setReplaceFile(true);
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload',
$file2->Name,
'File does not receive new name'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file2),
'File exists'
);
$this->assertEquals(
$file->ID,
$file2->ID,
'File database record is the same'
);
}
public function testReplaceFileWithLoadIntoFile()
{
// create tmp file
$tmpFileName = 'UploadTest-testUpload.txt';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'txt',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.txt',
$file->Name,
'File is uploaded without extension'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file),
'File exists'
);
// replace=true
$u = new Upload();
$u->setValidator($v);
$u->setReplaceFile(true);
$u->loadIntoFile($tmpFile, new File());
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.txt',
$file2->Name,
'File does not receive new name'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file2),
'File exists'
);
$this->assertEquals(
$file->ID,
$file2->ID,
'File database record is the same'
);
// replace=false
$u = new Upload();
$u->setValidator($v);
$u->setReplaceFile(false);
$u->loadIntoFile($tmpFile, new File());
$file3 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2.txt',
$file3->Name,
'File does receive new name'
);
$this->assertFileExists(
TestAssetStore::getLocalPath($file3),
'File exists'
);
$this->assertGreaterThan(
$file2->ID,
$file3->ID,
'File database record is not the same'
);
}
public function testDeleteResampledImagesOnUpload()
{
$tmpFileName = 'UploadTest-testUpload.jpg';
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$uploadImage = function () use ($tmpFileName) {
copy(__DIR__ . '/GDTest/images/test_jpg.jpg', $this->tmpFilePath);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'jpg',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
// test upload into default folder
$u = new Upload();
$u->setReplaceFile(true);
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
return $u->getFile();
};
// Image upload and generate a resampled image
$image = $uploadImage();
$resampled = $image->ResizedImage(123, 456);
$resampledPath = TestAssetStore::getLocalPath($resampled);
$this->assertFileExists($resampledPath);
// Re-upload the image, overwriting the original
// Resampled images should removed when their parent file is overwritten
$image = $uploadImage();
$this->assertFileExists($resampledPath);
}
public function testFileVersioningWithAnExistingFile()
{
$upload = function ($tmpFileName) {
// create tmp file
$this->tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = $this->getTemporaryFileContent();
file_put_contents($this->tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
$tmpFile = array(
'name' => $tmpFileName,
'type' => 'text/plaintext',
'size' => filesize($this->tmpFilePath),
'tmp_name' => $this->tmpFilePath,
'extension' => 'jpg',
'error' => UPLOAD_ERR_OK,
);
$v = new UploadTest\Validator();
// test upload into default folder
$u = new Upload();
$u->setReplaceFile(false);
$u->setValidator($v);
$u->loadIntoFile($tmpFile);
return $u->getFile();
};
// test empty file version prefix
Config::inst()->update('SilverStripe\\Assets\\Storage\\DefaultAssetNameGenerator', 'version_prefix', '');
$file1 = $upload('UploadTest-IMG001.jpg');
$this->assertEquals(
'UploadTest-IMG001.jpg',
$file1->Name,
'File does not receive new name'
);
$file2 = $upload('UploadTest-IMG001.jpg');
$this->assertEquals(
'UploadTest-IMG002.jpg',
$file2->Name,
'File does receive new name'
);
$file3 = $upload('UploadTest-IMG002.jpg');
$this->assertEquals(
'UploadTest-IMG003.jpg',
$file3->Name,
'File does receive new name'
);
$file4 = $upload('UploadTest-IMG3.jpg');
$this->assertEquals(
'UploadTest-IMG3.jpg',
$file4->Name,
'File does not receive new name'
);
$file1->delete();
$file2->delete();
$file3->delete();
$file4->delete();
// test '-v' file version prefix
Config::inst()->update('SilverStripe\\Assets\\Storage\\DefaultAssetNameGenerator', 'version_prefix', '-v');
$file1 = $upload('UploadTest2-IMG001.jpg');
$this->assertEquals(
'UploadTest2-IMG001.jpg',
$file1->Name,
'File does not receive new name'
);
$file2 = $upload('UploadTest2-IMG001.jpg');
$this->assertEquals(
'UploadTest2-IMG001-v2.jpg',
$file2->Name,
'File does receive new name'
);
$file3 = $upload('UploadTest2-IMG001.jpg');
$this->assertEquals(
'UploadTest2-IMG001-v3.jpg',
$file3->Name,
'File does receive new name'
);
$file4 = $upload('UploadTest2-IMG001-v3.jpg');
$this->assertEquals(
'UploadTest2-IMG001-v4.jpg',
$file4->Name,
'File does receive new name'
);
}
/**
* Generate some dummy file content
*
* @param int $reps How many zeros to return
* @return string
*/
protected function getTemporaryFileContent($reps = 10000)
{
return str_repeat('0', $reps);
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace SilverStripe\Assets\Tests\UploadTest;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Upload_Validator;
use SilverStripe\Dev\TestOnly;
class Validator extends Upload_Validator implements TestOnly
{
/**
* Looser check validation that doesn't do is_upload_file()
* checks as we're faking a POST request that PHP didn't generate
* itself.
*
* @return boolean
*/
public function validate()
{
$pathInfo = pathinfo($this->tmpFile['name']);
// filesize validation
if (!$this->isValidSize()) {
$ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : '';
$arg = File::format_size($this->getAllowedMaxFileSize($ext));
$this->errors[] = _t(
'File.TOOLARGE',
'File size is too large, maximum {size} allowed',
'Argument 1: File size (e.g. 1MB)',
array('size' => $arg)
);
return false;
}
// extension validation
if (!$this->isValidExtension()) {
$this->errors[] = _t('File.INVALIDEXTENSIONSHORT', 'Extension is not allowed');
return false;
}
return true;
}
}

View File

@ -1,101 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Assets\Storage\AssetStore;
use SilverStripe\Control\Director;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
class DBFileTest extends SapphireTest
{
protected $extraDataObjects = array(
DBFileTest\TestObject::class,
DBFileTest\Subclass::class,
);
protected $usesDatabase = true;
public function setUp()
{
parent::setUp();
// Set backend
TestAssetStore::activate('DBFileTest');
Director::config()->update('alternate_base_url', '/mysite/');
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
/**
* Test that images in a DBFile are rendered properly
*/
public function testRender()
{
$obj = new DBFileTest\TestObject();
// Test image tag
$fish = realpath(__DIR__ .'/../ORM/ImageTest/test-image-high-quality.jpg');
$this->assertFileExists($fish);
$obj->MyFile->setFromLocalFile($fish, 'awesome-fish.jpg');
$this->assertEquals(
'<img src="/mysite/assets/DBFileTest/a870de278b/awesome-fish.jpg" alt="awesome-fish.jpg" />',
trim($obj->MyFile->forTemplate())
);
// Test download tag
$obj->MyFile->setFromString('puppies', 'subdir/puppy-document.txt');
$this->assertEquals(
'<a href="/mysite/assets/DBFileTest/subdir/2a17a9cb4b/puppy-document.txt" title="puppy-document.txt" download="puppy-document.txt"/>',
trim($obj->MyFile->forTemplate())
);
}
public function testValidation()
{
$obj = new DBFileTest\ImageOnly();
// Test from image
$fish = realpath(__DIR__ .'/../ORM/ImageTest/test-image-high-quality.jpg');
$this->assertFileExists($fish);
$obj->MyFile->setFromLocalFile($fish, 'awesome-fish.jpg');
// This should fail
$this->setExpectedException('SilverStripe\\ORM\\ValidationException');
$obj->MyFile->setFromString('puppies', 'subdir/puppy-document.txt');
}
public function testPermission()
{
$obj = new DBFileTest\TestObject();
// Test from image
$fish = realpath(__DIR__ .'/../ORM/ImageTest/test-image-high-quality.jpg');
$this->assertFileExists($fish);
$obj->MyFile->setFromLocalFile(
$fish,
'private/awesome-fish.jpg',
null,
null,
array(
'visibility' => AssetStore::VISIBILITY_PROTECTED
)
);
// Test various file permissions work on DBFile
$this->assertFalse($obj->MyFile->canViewFile());
$obj->MyFile->getURL();
$this->assertTrue($obj->MyFile->canViewFile());
$obj->MyFile->revokeFile();
$this->assertFalse($obj->MyFile->canViewFile());
$obj->MyFile->getURL(false);
$this->assertFalse($obj->MyFile->canViewFile());
$obj->MyFile->grantFile();
$this->assertTrue($obj->MyFile->canViewFile());
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests\DBFileTest;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* @property DBFile $MyFile
*/
class ImageOnly extends DataObject implements TestOnly
{
private static $table_name = 'DBFileTest_ImageOnly';
private static $db = array(
"MyFile" => "DBFile('image/supported')"
);
}

View File

@ -1,18 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests\DBFileTest;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Dev\TestOnly;
/**
* @property DBFile $AnotherFile
*/
class Subclass extends TestObject implements TestOnly
{
private static $table_name = 'DBFileTest_Subclass';
private static $db = array(
"AnotherFile" => "DBFile"
);
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests\DBFileTest;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* @property DBFile $MyFile
*/
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'DBFileTest_TestObject';
private static $db = array(
"MyFile" => "DBFile"
);
}

View File

@ -13,7 +13,6 @@ use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
class DataDifferencerTest extends SapphireTest
{
protected static $fixture_file = 'DataDifferencerTest.yml';
protected $extraDataObjects = array(
@ -33,7 +32,7 @@ class DataDifferencerTest extends SapphireTest
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', Folder::class);
foreach ($files as $file) {
$fromPath = __DIR__ . '/ImageTest/' . $file->Name;
$fromPath = __DIR__ . '/DataDifferencerTest/images/' . $file->Name;
$destPath = TestAssetStore::getLocalPath($file); // Only correct for test asset store
Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -1,40 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
require_once __DIR__ . "/ImageTest.php";
use SilverStripe\Core\Config\Config;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Core\Injector\Injector;
class GDImageTest extends ImageTest
{
public function setUp()
{
parent::setUp();
if (!extension_loaded("gd")) {
$this->markTestSkipped("The GD extension is required");
return;
}
/**
* @skipUpgrade
*/
Config::inst()->update(
'SilverStripe\\Core\\Injector\\Injector',
'Image_Backend',
'SilverStripe\\Assets\\GDBackend'
);
}
public function tearDown()
{
$cache = Injector::inst()->get(CacheInterface::class . '.GDBackend_Manipulations');
$cache->clear();
parent::tearDown();
}
}

View File

@ -1,328 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem as SSFilesystem;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Image;
use SilverStripe\Assets\Storage\DBFile;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\View\Parsers\ShortcodeParser;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
class ImageTest extends SapphireTest
{
protected static $fixture_file = 'ImageTest.yml';
public function setUp()
{
parent::setUp();
// Execute specific subclass
if (get_class($this) == "ImageTest") {
$this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
return;
}
// Set backend root to /ImageTest
TestAssetStore::activate('ImageTest');
// Copy test images for each of the fixture references
$files = File::get()->exclude('ClassName', Folder::class);
foreach ($files as $image) {
$filePath = TestAssetStore::getLocalPath($image); // Only correct for test asset store
$sourcePath = __DIR__ . '/ImageTest/' . $image->Name;
if (!file_exists($filePath)) {
SSFilesystem::makeFolder(dirname($filePath));
if (!copy($sourcePath, $filePath)) {
user_error('Failed to copy test images', E_USER_ERROR);
}
}
}
}
public function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testGetTagWithTitle()
{
Config::inst()->update(DBFile::class, 'force_resample', false);
$image = $this->objFromFixture(Image::class, 'imageWithTitle');
$expected = '<img src="/assets/ImageTest/folder/444065542b/test-image.png" alt="This is a image Title" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
public function testGetTagWithoutTitle()
{
Config::inst()->update(DBFile::class, 'force_resample', false);
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$expected = '<img src="/assets/ImageTest/folder/444065542b/test-image.png" alt="test image" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
public function testGetTagWithoutTitleContainingDots()
{
Config::inst()->update(DBFile::class, 'force_resample', false);
$image = $this->objFromFixture(Image::class, 'imageWithoutTitleContainingDots');
$expected = '<img src="/assets/ImageTest/folder/46affab704/test.image.with.dots.png" alt="test.image.with.dots" />';
$actual = trim($image->getTag());
$this->assertEquals($expected, $actual);
}
/**
* Tests that multiple image manipulations may be performed on a single Image
*/
public function testMultipleGenerateManipulationCalls()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$imageFirst = $image->ScaleWidth(200);
$this->assertNotNull($imageFirst);
$expected = 200;
$actual = $imageFirst->getWidth();
$this->assertEquals($expected, $actual);
$imageSecond = $imageFirst->ScaleHeight(100);
$this->assertNotNull($imageSecond);
$expected = 100;
$actual = $imageSecond->getHeight();
$this->assertEquals($expected, $actual);
}
/**
* Tests that image manipulations that do not affect the resulting dimensions
* of the output image do not resample the file.
*/
public function testReluctanceToResampling()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$this->assertTrue($image->isSize(300, 300));
// Set width to 300 pixels
$imageScaleWidth = $image->ScaleWidth(300);
$this->assertEquals($imageScaleWidth->getWidth(), 300);
$this->assertEquals($image->Filename, $imageScaleWidth->Filename);
// Set height to 300 pixels
$imageScaleHeight = $image->ScaleHeight(300);
$this->assertEquals($imageScaleHeight->getHeight(), 300);
$this->assertEquals($image->Filename, $imageScaleHeight->Filename);
// Crop image to 300 x 300
$imageCropped = $image->Fill(300, 300);
$this->assertTrue($imageCropped->isSize(300, 300));
$this->assertEquals($image->Filename, $imageCropped->Filename);
// Resize (padded) to 300 x 300
$imageSized = $image->Pad(300, 300);
$this->assertTrue($imageSized->isSize(300, 300));
$this->assertEquals($image->Filename, $imageSized->Filename);
// Padded image 300 x 300 (same as above)
$imagePadded = $image->Pad(300, 300);
$this->assertTrue($imagePadded->isSize(300, 300));
$this->assertEquals($image->Filename, $imagePadded->Filename);
// Resized (stretched) to 300 x 300
$imageStretched = $image->ResizedImage(300, 300);
$this->assertTrue($imageStretched->isSize(300, 300));
$this->assertEquals($image->Filename, $imageStretched->Filename);
// Fit (various options)
$imageFit = $image->Fit(300, 600);
$this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageFit->Filename);
$imageFit = $image->Fit(600, 300);
$this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageFit->Filename);
$imageFit = $image->Fit(300, 300);
$this->assertTrue($imageFit->isSize(300, 300));
$this->assertEquals($image->Filename, $imageFit->Filename);
}
/**
* Tests that a URL to a resampled image is provided when force_resample is
* set to true, if the resampled file is smaller than the original.
*/
public function testForceResample()
{
$imageHQ = $this->objFromFixture(Image::class, 'highQualityJPEG');
$imageHQR = $imageHQ->Resampled();
$imageLQ = $this->objFromFixture(Image::class, 'lowQualityJPEG');
$imageLQR = $imageLQ->Resampled();
// Test resampled file is served when force_resample = true
Config::inst()->update(DBFile::class, 'force_resample', true);
$this->assertLessThan($imageHQ->getAbsoluteSize(), $imageHQR->getAbsoluteSize(), 'Resampled image is smaller than original');
$this->assertEquals($imageHQ->getURL(), $imageHQR->getSourceURL(), 'Path to a resampled image was returned by getURL()');
// Test original file is served when force_resample = true but original file is low quality
$this->assertGreaterThanOrEqual($imageLQ->getAbsoluteSize(), $imageLQR->getAbsoluteSize(), 'Resampled image is larger or same size as original');
$this->assertNotEquals($imageLQ->getURL(), $imageLQR->getSourceURL(), 'Path to the original image file was returned by getURL()');
// Test original file is served when force_resample = false
Config::inst()->update(DBFile::class, 'force_resample', false);
$this->assertNotEquals($imageHQ->getURL(), $imageHQR->getSourceURL(), 'Path to the original image file was returned by getURL()');
}
public function testImageResize()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$this->assertTrue($image->isSize(300, 300));
// Test normal resize
$resized = $image->Pad(150, 100);
$this->assertTrue($resized->isSize(150, 100));
// Test cropped resize
$cropped = $image->Fill(100, 200);
$this->assertTrue($cropped->isSize(100, 200));
// Test padded resize
$padded = $image->Pad(200, 100);
$this->assertTrue($padded->isSize(200, 100));
// Test Fit
$ratio = $image->Fit(80, 160);
$this->assertTrue($ratio->isSize(80, 80));
// Test FitMax
$fitMaxDn = $image->FitMax(200, 100);
$this->assertTrue($fitMaxDn->isSize(100, 100));
$fitMaxUp = $image->FitMax(500, 400);
$this->assertTrue($fitMaxUp->isSize(300, 300));
//Test ScaleMax
$scaleMaxWDn = $image->ScaleMaxWidth(200);
$this->assertTrue($scaleMaxWDn->isSize(200, 200));
$scaleMaxWUp = $image->ScaleMaxWidth(400);
$this->assertTrue($scaleMaxWUp->isSize(300, 300));
$scaleMaxHDn = $image->ScaleMaxHeight(200);
$this->assertTrue($scaleMaxHDn->isSize(200, 200));
$scaleMaxHUp = $image->ScaleMaxHeight(400);
$this->assertTrue($scaleMaxHUp->isSize(300, 300));
// Test FillMax
$cropMaxDn = $image->FillMax(200, 100);
$this->assertTrue($cropMaxDn->isSize(200, 100));
$cropMaxUp = $image->FillMax(400, 200);
$this->assertTrue($cropMaxUp->isSize(300, 150));
// Test Clip
$clipWDn = $image->CropWidth(200);
$this->assertTrue($clipWDn->isSize(200, 300));
$clipWUp = $image->CropWidth(400);
$this->assertTrue($clipWUp->isSize(300, 300));
$clipHDn = $image->CropHeight(200);
$this->assertTrue($clipHDn->isSize(300, 200));
$clipHUp = $image->CropHeight(400);
$this->assertTrue($clipHUp->isSize(300, 300));
}
/**
* @expectedException InvalidArgumentException
*/
public function testGenerateImageWithInvalidParameters()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$image->ScaleHeight('String');
$image->Pad(600, 600, 'XXXXXX');
}
public function testCacheFilename()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$imageFirst = $image->Pad(200, 200, 'CCCCCC');
$imageFilename = $imageFirst->getURL();
// Encoding of the arguments is duplicated from cacheFilename
$neededPart = 'Pad' . Convert::base64url_encode(array(200,200,'CCCCCC'));
$this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated');
}
/**
* Test that propertes from the source Image are inherited by resampled images
*/
public function testPropertyInheritance()
{
$testString = 'This is a test';
$origImage = $this->objFromFixture(Image::class, 'imageWithTitle');
$origImage->TestProperty = $testString;
$resampled = $origImage->ScaleWidth(10);
$this->assertEquals($resampled->TestProperty, $testString);
$resampled2 = $resampled->ScaleWidth(5);
$this->assertEquals($resampled2->TestProperty, $testString);
}
public function testShortcodeHandlerFallsBackToFileProperties()
{
$image = $this->objFromFixture(Image::class, 'imageWithTitle');
$parser = new ShortcodeParser();
$parser->register('image', array(Image::class, 'handle_shortcode'));
$this->assertEquals(
sprintf(
'<img src="%s" alt="%s">',
$image->Link(),
$image->Title
),
$parser->parse(sprintf('[image id=%d]', $image->ID))
);
}
public function testShortcodeHandlerUsesShortcodeProperties()
{
$image = $this->objFromFixture(Image::class, 'imageWithTitle');
$parser = new ShortcodeParser();
$parser->register('image', array(Image::class, 'handle_shortcode'));
$this->assertEquals(
sprintf(
'<img src="%s" alt="Alt content" title="Title content">',
$image->Link()
),
$parser->parse(
sprintf(
'[image id="%d" alt="Alt content" title="Title content"]',
$image->ID
)
)
);
}
public function testShortcodeHandlerAddsDefaultAttributes()
{
$image = $this->objFromFixture(Image::class, 'imageWithoutTitle');
$parser = new ShortcodeParser();
$parser->register('image', array(Image::class, 'handle_shortcode'));
$this->assertEquals(
sprintf(
'<img src="%s" alt="%s">',
$image->Link(),
$image->Title
),
$parser->parse(
sprintf(
'[image id="%d"]',
$image->ID
)
)
);
}
}

View File

@ -1,38 +0,0 @@
SilverStripe\Assets\Folder:
folder1:
Name: folder
SilverStripe\Assets\Image:
imageWithTitle:
Title: This is a image Title
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test-image.png
imageWithoutTitle:
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test-image.png
imageWithoutTitleContainingDots:
FileFilename: folder/test.image.with.dots.png
FileHash: 46affab7043cfd9f1ded919dd24affd08e926eca
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test.image.with.dots.png
imageWithMetacharacters:
Title: This is a/an image Title
FileFilename: folder/test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test-image.png
lowQualityJPEG:
Title: This is a low quality JPEG
FileFilename: folder/test-image-low-quality.jpg
FileHash: 33be1b95cba0358fe54e8b13532162d52f97421c
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test-image-low-quality.jpg
highQualityJPEG:
Title: This is a high quality JPEG
FileFilename: folder/test-image-high-quality.jpg
FileHash: a870de278b475cb75f5d9f451439b2d378e13af1
Parent: =>SilverStripe\Assets\Folder.folder1
Name: test-image-high-quality.jpg

View File

@ -1,25 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Assets\ImagickBackend;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
class ImagickImageTest extends ImageTest
{
public function setUp()
{
parent::setUp();
if (!extension_loaded("imagick")) {
$this->markTestSkipped("The Imagick extension is not available.");
return;
}
/**
* @skipUpgrade
*/
Config::inst()->update(Injector::class, 'Image_Backend', ImagickBackend::class);
}
}