API Split SilverStripe\Assets into separate module
@ -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:
|
||||
|
58
.upgrade.yml
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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/",
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
1320
src/Assets/File.php
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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] == '/';
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ trait Extensible
|
||||
* @var array $extensions
|
||||
* @config
|
||||
*/
|
||||
private static $extensions = null;
|
||||
private static $extensions = [];
|
||||
|
||||
private static $classes_constructed = array();
|
||||
|
||||
|
@ -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.
|
||||
|
@ -1 +0,0 @@
|
||||
<a href="$URL.ATT" title="$Title" <% if $Basename %>download="$Basename.ATT"<% else %>download<% end_if %>/>
|
@ -1 +0,0 @@
|
||||
<img src="$URL.ATT" alt="$Title.ATT" />
|
@ -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>
|
@ -1,2 +0,0 @@
|
||||
Deny from all
|
||||
RewriteRule .* - [F]
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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');
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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'
|
@ -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';
|
||||
}
|
@ -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'));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
No this is not an image file
|
Before Width: | Height: | Size: 132 B |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 253 B |
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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'])
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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')"
|
||||
);
|
||||
}
|
@ -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"
|
||||
);
|
||||
}
|
@ -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"
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|