mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #5001 from open-sausages/pulls/4.0/versioned-asset-control
API File is now Versioned
This commit is contained in:
commit
c96dd629e1
6
_config/versioning.yml
Normal file
6
_config/versioning.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
Name: versioning
|
||||
---
|
||||
GridFieldDetailForm:
|
||||
extensions:
|
||||
- VersionedGridFieldDetailForm
|
@ -460,6 +460,7 @@ class RequestHandler extends ViewableData {
|
||||
* @param int $errorCode
|
||||
* @param string $errorMessage Plaintext error message
|
||||
* @uses SS_HTTPResponse_Exception
|
||||
* @throws SS_HTTPResponse_Exception
|
||||
*/
|
||||
public function httpError($errorCode, $errorMessage = null) {
|
||||
|
||||
|
@ -169,12 +169,21 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* State of Versioned before this test is run
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $originalReadingMode = null;
|
||||
|
||||
public function setUp() {
|
||||
|
||||
//nest config and injector for each test so they are effectively sandboxed per test
|
||||
Config::nest();
|
||||
Injector::nest();
|
||||
|
||||
$this->originalReadingMode = \Versioned::get_reading_mode();
|
||||
|
||||
// We cannot run the tests on this abstract class.
|
||||
if(get_class($this) == "SapphireTest") $this->skipTest = true;
|
||||
|
||||
@ -525,6 +534,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$controller->response->setStatusCode(200);
|
||||
$controller->response->removeHeader('Location');
|
||||
}
|
||||
|
||||
\Versioned::set_reading_mode($this->originalReadingMode);
|
||||
|
||||
//unnest injector / config now that tests are over
|
||||
Injector::unnest();
|
||||
Config::unnest();
|
||||
|
@ -278,23 +278,27 @@ You can customise this with the below config:
|
||||
### Configuring: Archive behaviour
|
||||
|
||||
By default, the default extension `AssetControlExtension` will control the disposal of assets
|
||||
attached to objects when those objects are deleted. For example, unpublished versioned objects
|
||||
will automatically have their attached assets moved to the protected store. The deletion of
|
||||
draft or unversioned objects will have those assets permanantly deleted (along with all variants).
|
||||
attached to objects when those objects are archived. For example, unpublished versioned objects
|
||||
will automatically have their attached assets moved to the protected store. The archive of
|
||||
draft or (or deletion of unversioned objects) will have those assets permanantly deleted
|
||||
(along with all variants).
|
||||
|
||||
In some cases, it may be preferable to have any deleted assets archived for versioned dataobjects,
|
||||
rather than deleted. This uses more disk storage, but will allow the full recovery of archived
|
||||
Note that regardless of this setting, the database record will still be archived in the
|
||||
version history for all Versioned DataObjects.
|
||||
|
||||
In some cases, it may be preferable to have any assets retained for archived versioned dataobjects,
|
||||
instead of deleting them. This uses more disk storage, but will allow the full recovery of archived
|
||||
records and files.
|
||||
|
||||
This can be applied to DataObjects on a case by case basis by setting the `archive_assets`
|
||||
This can be applied to DataObjects on a case by case basis by setting the `keep_archived_assets`
|
||||
config to true on that class. Note that this feature only works with dataobjects with
|
||||
the `Versioned` extension.
|
||||
|
||||
|
||||
:::php
|
||||
class MyVersiondObject extends DataObject {
|
||||
/** Enable archiving */
|
||||
private static $archive_assets = true;
|
||||
/** Ensure assets are archived along with the DataObject */
|
||||
private static $keep_archived_assets = true;
|
||||
/** Versioned */
|
||||
private static $extensions = array('Versioned');
|
||||
}
|
||||
|
@ -25,6 +25,9 @@
|
||||
more information.
|
||||
* `Object::useCustomClass` has been removed. You should use the config API with Injector instead.
|
||||
* Upgrade of TinyMCE to version 4.
|
||||
* `File` is now versioned, and should be published before they can be used on the frontend.
|
||||
See section on [Migrating File DataObject from 3.x to 4.0](#migrating-file-dataobject-from-3x-to-40)
|
||||
below for upgrade notes.
|
||||
|
||||
## New API
|
||||
|
||||
@ -48,6 +51,12 @@
|
||||
TinyMCE editor.
|
||||
* `HtmlEditorField::setEditorConfig` may now take an instance of a `HtmlEditorConfig` class, as well as a
|
||||
standard config identifier name.
|
||||
* A lot of standard versioned API has been refactored from `SiteTree` into `Versioned` extension. Now
|
||||
all versioned DataObjects have canPublish(), canArchive(), canUnpublish(), doPublish(), doArchive()
|
||||
doUnpublish(), isPublished() and isonDraft() out of the box. However, do*() methods will no longer
|
||||
automatically check can*() permissions, and must be done by usercode before invocation.
|
||||
* GridField edit form now has improved support for versioned DataObjects, with basic publishing
|
||||
actions available when editing records.
|
||||
|
||||
### Front-end build tooling for CMS interface
|
||||
|
||||
@ -243,6 +252,13 @@ large amounts of memory and run for an extended time.
|
||||
migrate_legacy_file: true
|
||||
|
||||
|
||||
This task will also support migration of existing File DataObjects to file versioning. Any
|
||||
pre-existing File DataObjects will be automatically published to the live stage, to ensure
|
||||
that previously visible assets remain visible to the public site.
|
||||
|
||||
If additional security or visibility rules should be applied to File dataobjects, then
|
||||
make sure to correctly extend `canView` via extensions.
|
||||
|
||||
### Upgrade code which acts on `Image`
|
||||
|
||||
As all image-specific manipulations has been refactored from `Image` into an `ImageManipulations` trait, which
|
||||
@ -321,9 +337,11 @@ After:
|
||||
|
||||
:::php
|
||||
function importTempFile($tmp) {
|
||||
Versioned::reading_stage('Stage');
|
||||
$file = new File();
|
||||
$file->setFromLocalFile($tmp, 'imported/'.basename($tmp));
|
||||
$file->write();
|
||||
$file->doPublish();
|
||||
}
|
||||
|
||||
|
||||
@ -333,13 +351,19 @@ stored in the assets folder.
|
||||
|
||||
There are other important considerations in working with File dataobjects which differ from legacy:
|
||||
|
||||
* Deleting File dataobjects no longer removes the physical file directly. This is because any file could be referenced
|
||||
from DBFile fields, and deleting these could be a potentially unsafe operation.
|
||||
* File synchronisation is no longer automatic. This is due to the fact that there is no longer a 1-to-1 relationship
|
||||
between physical files and File dataobjects.
|
||||
* Moving files now performs a file copy rather than moving the underlying file, although only a single DataObject
|
||||
will exist, and will reference the destination path.
|
||||
* Folder dataobjects are now purely logical dataobjects, and perform no actual filesystem folder creation on write.
|
||||
* All Files are versioned, which means that by default, new File records will not be visibile
|
||||
to the public site. You will need to make sure to invoke ->doPublish() on any File dataobject
|
||||
you wish visitors to be able to see.
|
||||
|
||||
You can disable File versioning by adding the following to your _config.php
|
||||
|
||||
|
||||
:::php
|
||||
File::remove_extension('Versioned');
|
||||
|
||||
|
||||
### Upgrading code performs custom image manipulations
|
||||
|
||||
|
@ -4,110 +4,294 @@ namespace SilverStripe\Filesystem;
|
||||
|
||||
use DataObject;
|
||||
use Injector;
|
||||
use Member;
|
||||
use Versioned;
|
||||
use SilverStripe\Filesystem\Storage\AssetStore;
|
||||
|
||||
/**
|
||||
* Provides an interaction mechanism between objects and linked asset references.
|
||||
* 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 {
|
||||
class AssetControlExtension extends \DataExtension
|
||||
{
|
||||
|
||||
/**
|
||||
* When archiving versioned dataobjects, should assets be archived with them?
|
||||
* If false, assets will be deleted when the object is removed from draft.
|
||||
* If true, assets will be instead moved to the protected store.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $archive_assets = false;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public function onAfterDelete() {
|
||||
$assets = $this->findAssets($this->owner);
|
||||
/**
|
||||
* Ensure that deletes records remove their underlying file assets, without affecting
|
||||
* other staged records.
|
||||
*/
|
||||
public function onAfterDelete()
|
||||
{
|
||||
// Prepare blank manipulation
|
||||
$manipulations = new AssetManipulationList();
|
||||
|
||||
// When deleting from live, just secure assets
|
||||
// Note that DataObject::delete() ignores sourceQueryParams
|
||||
if($this->isVersioned() && \Versioned::current_stage() === \Versioned::get_live_stage()) {
|
||||
$this->protectAll($assets);
|
||||
return;
|
||||
}
|
||||
// Add all assets for deletion
|
||||
$this->addAssetsFromRecord($manipulations, $this->owner, AssetManipulationList::STATE_DELETED);
|
||||
|
||||
// When deleting from stage then check if we should archive assets
|
||||
$archive = $this->owner->config()->archive_assets;
|
||||
if($archive && $this->isVersioned()) {
|
||||
// Archived assets are kept protected
|
||||
$this->protectAll($assets);
|
||||
} else {
|
||||
// Otherwise remove all assets
|
||||
$this->deleteAll($assets);
|
||||
}
|
||||
}
|
||||
// Whitelist assets that exist in other stages
|
||||
$this->addAssetsFromOtherStages($manipulations);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
foreach($record->db() as $field => $db) {
|
||||
// Extract assets from this database field
|
||||
list($dbClass) = explode('(', $db);
|
||||
if(!is_a($dbClass, 'DBFile', true)) {
|
||||
continue;
|
||||
}
|
||||
// Apply visibility rules based on the final manipulation
|
||||
$this->processManipulation($manipulations);
|
||||
}
|
||||
|
||||
// Omit variant and merge with set
|
||||
$next = $record->dbObject($field)->getValue();
|
||||
unset($next['Variant']);
|
||||
if ($next) {
|
||||
$files[] = $next;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Ensure that changes to records flush overwritten files, and update the visibility
|
||||
* of other assets.
|
||||
*/
|
||||
public function onBeforeWrite()
|
||||
{
|
||||
// Prepare blank manipulation
|
||||
$manipulations = new AssetManipulationList();
|
||||
|
||||
// De-dupe
|
||||
return array_map("unserialize", array_unique(array_map("serialize", $files)));
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if versioning rules should be applied to this object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isVersioned() {
|
||||
return $this->owner->has_extension('Versioned');
|
||||
}
|
||||
// Add assets from new record with the correct visibility rules
|
||||
$state = $this->getRecordState($this->owner);
|
||||
$this->addAssetsFromRecord($manipulations, $this->owner, $state);
|
||||
|
||||
/**
|
||||
* Delete all assets in the tuple list
|
||||
*
|
||||
* @param array $assets
|
||||
*/
|
||||
protected function deleteAll($assets) {
|
||||
$store = $this->getAssetStore();
|
||||
foreach($assets as $asset) {
|
||||
$store->delete($asset['Filename'], $asset['Hash']);
|
||||
}
|
||||
}
|
||||
// Whitelist assets that exist in other stages
|
||||
$this->addAssetsFromOtherStages($manipulations);
|
||||
|
||||
/**
|
||||
* Move all assets in the list to the protected store
|
||||
*
|
||||
* @param array $assets
|
||||
*/
|
||||
protected function protectAll($assets) {
|
||||
$store = $this->getAssetStore();
|
||||
foreach($assets as $asset) {
|
||||
$store->protect($asset['Filename'], $asset['Hash']);
|
||||
}
|
||||
}
|
||||
// Apply visibility rules based on the final manipulation
|
||||
$this->processManipulation($manipulations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AssetStore
|
||||
*/
|
||||
protected function getAssetStore() {
|
||||
return Injector::inst()->get('AssetStore');
|
||||
}
|
||||
/**
|
||||
* 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::current_stage();
|
||||
|
||||
// Non-live stages are automatically non-public
|
||||
if($stage !== Versioned::get_live_stage()) {
|
||||
return AssetManipulationList::STATE_PROTECTED;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if canView permits anonymous viewers
|
||||
return $record->canView(Member::create())
|
||||
? 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()->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 = \ClassInfo::baseDataClass($this->owner);
|
||||
$filter = array("\"{$baseClass}\".\"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::current_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();
|
||||
foreach ($record->db() as $field => $db) {
|
||||
// Extract assets from this database field
|
||||
list($dbClass) = explode('(', $db);
|
||||
if (!is_a($dbClass, 'DBFile', true)) {
|
||||
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('Versioned') && class_exists('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');
|
||||
}
|
||||
}
|
||||
|
175
filesystem/AssetManipulationList.php
Normal file
175
filesystem/AssetManipulationList.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Filesystem;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -61,16 +61,27 @@ use SilverStripe\Filesystem\Storage\AssetStore;
|
||||
*
|
||||
* @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 {
|
||||
|
||||
use ImageManipulation;
|
||||
|
||||
private static $default_sort ="\"Name\"";
|
||||
private static $default_sort = "\"Name\"";
|
||||
|
||||
private static $singular_name ="File";
|
||||
private static $singular_name = "File";
|
||||
|
||||
private static $plural_name ="Files";
|
||||
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)",
|
||||
@ -81,8 +92,8 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
);
|
||||
|
||||
private static $has_one = array(
|
||||
"Parent" =>"File",
|
||||
"Owner" =>"Member"
|
||||
"Parent" => "File",
|
||||
"Owner" => "Member"
|
||||
);
|
||||
|
||||
private static $defaults = array(
|
||||
@ -91,6 +102,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
|
||||
private static $extensions = array(
|
||||
"Hierarchy",
|
||||
"Versioned"
|
||||
);
|
||||
|
||||
private static $casting = array(
|
||||
@ -202,18 +214,30 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
* @return string Result of the handled shortcode
|
||||
*/
|
||||
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
|
||||
if(!isset($arguments['id']) || !is_numeric($arguments['id'])) return;
|
||||
if(!isset($arguments['id']) || !is_numeric($arguments['id'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var File|SiteTree $record */
|
||||
$record = DataObject::get_by_id('File', $arguments['id']);
|
||||
|
||||
// Check record for common errors
|
||||
$errorCode = null;
|
||||
if (!$record) {
|
||||
if(class_exists('ErrorPage')) {
|
||||
$record = ErrorPage::get()->filter("ErrorCode", 404)->first();
|
||||
$errorCode = 404;
|
||||
} elseif(!$record->canView()) {
|
||||
$errorCode = 403;
|
||||
}
|
||||
if($errorCode) {
|
||||
$result = static::singleton()->invokeWithExtensions('getErrorRecordFor', $errorCode);
|
||||
$result = array_filter($result);
|
||||
if($result) {
|
||||
$record = reset($result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$record) {
|
||||
return; // There were no suitable matches at all.
|
||||
}
|
||||
if (!$record) {
|
||||
return null; // There were no suitable matches at all.
|
||||
}
|
||||
|
||||
// build the HTML tag
|
||||
@ -305,10 +329,8 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Enforce on filesystem URL level via mod_rewrite
|
||||
*
|
||||
* @param Member $member
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function canView($member = null) {
|
||||
if(!$member) {
|
||||
@ -401,7 +423,7 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
ReadonlyField::create(
|
||||
'ClickableURL',
|
||||
_t('AssetTableField.URL','URL'),
|
||||
sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->RelativeLink())
|
||||
sprintf('<a href="%s" target="_blank">%s</a>', $this->Link(), $this->Link())
|
||||
)
|
||||
->setDontEscape(true),
|
||||
new DateField_Disabled("Created", _t('AssetTableField.CREATED','First uploaded') . ':'),
|
||||
@ -505,8 +527,6 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
* Make sure the file has a name
|
||||
*/
|
||||
protected function onBeforeWrite() {
|
||||
parent::onBeforeWrite();
|
||||
|
||||
// Set default owner
|
||||
if(!$this->isInDB() && !$this->OwnerID) {
|
||||
$this->OwnerID = Member::currentUserID();
|
||||
@ -519,6 +539,8 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
|
||||
// Propegate changes to the AssetStore and update the DBFile field
|
||||
$this->updateFilesystem();
|
||||
|
||||
parent::onBeforeWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -562,12 +584,6 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function onAfterWrite() {
|
||||
parent::onAfterWrite();
|
||||
// Update any database references
|
||||
$this->updateLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collate selected descendants of this page.
|
||||
* $condition will be evaluated on each descendant, and if it is succeeds, that item will be added
|
||||
@ -642,15 +658,6 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger update of all links to this file
|
||||
*
|
||||
* If CMS Module is installed, {@see SiteTreeFileExtension::updateLinks}
|
||||
*/
|
||||
protected function updateLinks() {
|
||||
$this->extend('updateLinks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of this file
|
||||
*
|
||||
@ -707,6 +714,19 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer {
|
||||
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() {
|
||||
// Relies on Parent() returning the stage record
|
||||
$parent = $this->Parent();
|
||||
if($parent && $parent->exists()) {
|
||||
$parent->doPublish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the ParentID and Name for the given filename.
|
||||
*
|
||||
|
@ -34,6 +34,8 @@ class FileMigrationHelper extends Object {
|
||||
|
||||
// Loop over all files
|
||||
$count = 0;
|
||||
$originalState = \Versioned::get_reading_mode();
|
||||
\Versioned::reading_stage('Stage');
|
||||
$filenameMap = $this->getFilenameArray();
|
||||
foreach($this->getFileQuery() as $file) {
|
||||
// Get the name of the file to import
|
||||
@ -43,6 +45,7 @@ class FileMigrationHelper extends Object {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
\Versioned::set_reading_mode($originalState);
|
||||
return $count;
|
||||
}
|
||||
|
||||
@ -73,8 +76,9 @@ class FileMigrationHelper extends Object {
|
||||
$this->setFilename($result['Filename']);
|
||||
}
|
||||
|
||||
// Save
|
||||
// Save and publish
|
||||
$file->write();
|
||||
$file->doPublish();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,13 @@ class Upload extends Controller {
|
||||
*/
|
||||
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.
|
||||
@ -198,7 +205,10 @@ class Upload extends Controller {
|
||||
$conflictResolution = $this->replaceFile
|
||||
? AssetStore::CONFLICT_OVERWRITE
|
||||
: AssetStore::CONFLICT_RENAME;
|
||||
$config = array('conflict' => $conflictResolution);
|
||||
$config = array(
|
||||
'conflict' => $conflictResolution,
|
||||
'visibility' => $this->getDefaultVisibility()
|
||||
);
|
||||
return $container->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $config);
|
||||
}
|
||||
|
||||
@ -210,7 +220,7 @@ class Upload extends Controller {
|
||||
* @param string $folderPath
|
||||
* @return string|false Value of filename tuple, or false if invalid
|
||||
*/
|
||||
protected function getValidFilename($tmpFile, $folderPath = false) {
|
||||
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"
|
||||
@ -245,6 +255,7 @@ class Upload extends Controller {
|
||||
*
|
||||
* @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)
|
||||
@ -252,7 +263,7 @@ class Upload extends Controller {
|
||||
$fileClass = File::get_class_for_file_extension(
|
||||
File::get_file_extension($filename)
|
||||
);
|
||||
$this->file = $fileClass::create();
|
||||
$this->file = Object::create($fileClass);
|
||||
}
|
||||
|
||||
// Skip this step if not writing File dataobjects
|
||||
@ -288,14 +299,14 @@ class Upload extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean
|
||||
* @param bool $replace
|
||||
*/
|
||||
public function setReplaceFile($bool) {
|
||||
$this->replaceFile = $bool;
|
||||
public function setReplaceFile($replace) {
|
||||
$this->replaceFile = $replace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function getReplaceFile() {
|
||||
return $this->replaceFile;
|
||||
@ -368,6 +379,28 @@ class Upload extends Controller {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -497,7 +530,6 @@ class Upload_Validator {
|
||||
// make sure all extensions are lowercase
|
||||
$rules = array_change_key_case($rules, CASE_LOWER);
|
||||
$finalRules = array();
|
||||
$tmpSize = 0;
|
||||
|
||||
foreach ($rules as $rule => $value) {
|
||||
if (is_numeric($value)) {
|
||||
@ -534,7 +566,9 @@ class Upload_Validator {
|
||||
* @param array $rules List of extensions
|
||||
*/
|
||||
public function setAllowedExtensions($rules) {
|
||||
if(!is_array($rules)) return false;
|
||||
if(!is_array($rules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure all rules are lowercase
|
||||
foreach($rules as &$rule) $rule = strtolower($rule);
|
||||
|
@ -17,7 +17,7 @@ use SilverStripe\Filesystem\Storage\AssetStore;
|
||||
* @package framework
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandler {
|
||||
class DBFile extends CompositeDBField implements AssetContainer {
|
||||
|
||||
use ImageManipulation;
|
||||
|
||||
@ -311,14 +311,6 @@ class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandle
|
||||
->getStore()
|
||||
->exists($this->Filename, $this->Hash, $this->Variant);
|
||||
}
|
||||
|
||||
public static function get_shortcodes() {
|
||||
return 'dbfile_link';
|
||||
}
|
||||
|
||||
public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
|
||||
// @todo
|
||||
}
|
||||
|
||||
public function getFilename() {
|
||||
return $this->getField('Filename');
|
||||
|
@ -313,6 +313,9 @@ class FieldList extends ArrayList {
|
||||
* You can use dot syntax to get fields from child composite fields
|
||||
*
|
||||
* @todo Implement similarly to dataFieldByName() to support nested sets - or merge with dataFields()
|
||||
*
|
||||
* @param string $name
|
||||
* @return FormField
|
||||
*/
|
||||
public function fieldByName($name) {
|
||||
$name = $this->rewriteTabPath($name);
|
||||
|
@ -1142,6 +1142,12 @@ class UploadField extends FileField {
|
||||
// Search for relations that can hold the uploaded files, but don't fallback
|
||||
// to default if there is no automatic relation
|
||||
if ($relationClass = $this->getRelationAutosetClass(null)) {
|
||||
// Allow File to be subclassed
|
||||
if($relationClass === 'File' && isset($tmpFile['name'])) {
|
||||
$relationClass = File::get_class_for_file_extension(
|
||||
File::get_file_extension($tmpFile['name'])
|
||||
);
|
||||
}
|
||||
// Create new object explicitly. Otherwise rely on Upload::load to choose the class.
|
||||
$fileObject = Object::create($relationClass);
|
||||
if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
use SilverStripe\Framework\Core\Extensible;
|
||||
|
||||
/**
|
||||
* Provides view and edit forms at GridField-specific URLs.
|
||||
@ -18,8 +19,10 @@
|
||||
*/
|
||||
class GridFieldDetailForm implements GridField_URLHandler {
|
||||
|
||||
use Extensible;
|
||||
|
||||
/**
|
||||
* @var String
|
||||
* @var string
|
||||
*/
|
||||
protected $template = 'GridFieldDetailForm';
|
||||
|
||||
@ -40,12 +43,12 @@ class GridFieldDetailForm implements GridField_URLHandler {
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* @var String
|
||||
* @var string
|
||||
*/
|
||||
protected $itemRequestClass;
|
||||
|
||||
/**
|
||||
* @var function With two parameters: $form and $component
|
||||
* @var callable With two parameters: $form and $component
|
||||
*/
|
||||
protected $itemEditFormCallback;
|
||||
|
||||
@ -68,6 +71,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
|
||||
*/
|
||||
public function __construct($name = 'DetailForm') {
|
||||
$this->name = $name;
|
||||
$this->constructExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,10 +92,7 @@ class GridFieldDetailForm implements GridField_URLHandler {
|
||||
$record = Object::create($gridField->getModelClass());
|
||||
}
|
||||
|
||||
$class = $this->getItemRequestClass();
|
||||
|
||||
$handler = Object::create($class, $gridField, $this, $record, $requestHandler, $this->name);
|
||||
$handler->setTemplate($this->template);
|
||||
$handler = $this->getItemRequestHandler($gridField, $record, $requestHandler);
|
||||
|
||||
// if no validator has been set on the GridField and the record has a
|
||||
// CMS validator, use that.
|
||||
@ -102,6 +103,26 @@ class GridFieldDetailForm implements GridField_URLHandler {
|
||||
return $handler->handleRequest($request, DataModel::inst());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a request handler for the given record
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param DataObject $record
|
||||
* @param Controller $requestHandler
|
||||
* @return GridFieldDetailForm_ItemRequest
|
||||
*/
|
||||
protected function getItemRequestHandler($gridField, $record, $requestHandler) {
|
||||
$class = $this->getItemRequestClass();
|
||||
$this->extend('updateItemRequestClass', $class, $gridField, $record, $requestHandler);
|
||||
$handler = \Injector::inst()->createWithArgs(
|
||||
$class,
|
||||
array($gridField, $this, $record, $requestHandler, $this->name)
|
||||
);
|
||||
$handler->setTemplate($this->template);
|
||||
$this->extend('updateItemRequestHandler', $handler);
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String
|
||||
*/
|
||||
@ -351,41 +372,8 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
return $controller->httpError(403);
|
||||
}
|
||||
|
||||
$actions = new FieldList();
|
||||
if($this->record->ID !== 0) {
|
||||
if($canEdit) {
|
||||
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-constructive')
|
||||
->setAttribute('data-icon', 'accept'));
|
||||
}
|
||||
|
||||
if($canDelete) {
|
||||
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-destructive action-delete'));
|
||||
}
|
||||
|
||||
}else{ // adding new record
|
||||
//Change the Save label to 'Create'
|
||||
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-constructive')
|
||||
->setAttribute('data-icon', 'add'));
|
||||
|
||||
// Add a Cancel link which is a button-like link and link back to one level up.
|
||||
$curmbs = $this->Breadcrumbs();
|
||||
if($curmbs && $curmbs->count()>=2){
|
||||
$one_level_up = $curmbs->offsetGet($curmbs->count()-2);
|
||||
$text = sprintf(
|
||||
"<a class=\"%s\" href=\"%s\">%s</a>",
|
||||
"crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
|
||||
$one_level_up->Link, // url
|
||||
_t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
|
||||
);
|
||||
$actions->push(new LiteralField('cancelbutton', $text));
|
||||
}
|
||||
}
|
||||
// Build actions
|
||||
$actions = $this->getFormActions();
|
||||
|
||||
$fields = $this->component->getFields();
|
||||
if(!$fields) $fields = $this->record->getCMSFields();
|
||||
@ -462,6 +450,53 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the set of form field actions for this DataObject
|
||||
*
|
||||
* @return FieldList
|
||||
*/
|
||||
protected function getFormActions() {
|
||||
$canEdit = $this->record->canEdit();
|
||||
$canDelete = $this->record->canDelete();
|
||||
$actions = new FieldList();
|
||||
if($this->record->ID !== 0) {
|
||||
if($canEdit) {
|
||||
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Save', 'Save'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-constructive')
|
||||
->setAttribute('data-icon', 'accept'));
|
||||
}
|
||||
|
||||
if($canDelete) {
|
||||
$actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-destructive action-delete'));
|
||||
}
|
||||
|
||||
} else { // adding new record
|
||||
//Change the Save label to 'Create'
|
||||
$actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create'))
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-constructive')
|
||||
->setAttribute('data-icon', 'add'));
|
||||
|
||||
// Add a Cancel link which is a button-like link and link back to one level up.
|
||||
$crumbs = $this->Breadcrumbs();
|
||||
if($crumbs && $crumbs->count() >= 2){
|
||||
$oneLevelUp = $crumbs->offsetGet($crumbs->count() - 2);
|
||||
$text = sprintf(
|
||||
"<a class=\"%s\" href=\"%s\">%s</a>",
|
||||
"crumb ss-ui-button ss-ui-action-destructive cms-panel-link ui-corner-all", // CSS classes
|
||||
$oneLevelUp->Link, // url
|
||||
_t('GridFieldDetailForm.CancelBtn', 'Cancel') // label
|
||||
);
|
||||
$actions->push(new LiteralField('cancelbutton', $text));
|
||||
}
|
||||
}
|
||||
$this->extend('updateFormActions', $actions);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse the nested RequestHandlers until we reach something that's not GridFieldDetailForm_ItemRequest.
|
||||
* This allows us to access the Controller responsible for invoking the top-level GridField.
|
||||
@ -525,47 +560,20 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
}
|
||||
|
||||
public function doSave($data, $form) {
|
||||
$new_record = $this->record->ID == 0;
|
||||
$controller = $this->getToplevelController();
|
||||
$list = $this->gridField->getList();
|
||||
$isNewRecord = $this->record->ID == 0;
|
||||
|
||||
if(!$this->record->canEdit()) {
|
||||
return $controller->httpError(403);
|
||||
}
|
||||
|
||||
if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
|
||||
$newClassName = $data['ClassName'];
|
||||
// The records originally saved attribute was overwritten by $form->saveInto($record) before.
|
||||
// This is necessary for newClassInstance() to work as expected, and trigger change detection
|
||||
// on the ClassName attribute
|
||||
$this->record->setClassName($this->record->ClassName);
|
||||
// Replace $record with a new instance
|
||||
$this->record = $this->record->newClassInstance($newClassName);
|
||||
// Check permission
|
||||
if (!$this->record->canEdit()) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
|
||||
// Save from form data
|
||||
try {
|
||||
$form->saveInto($this->record);
|
||||
$this->record->write();
|
||||
$extraData = $this->getExtraSavedData($this->record, $list);
|
||||
$list->add($this->record, $extraData);
|
||||
} catch(ValidationException $e) {
|
||||
$form->sessionMessage($e->getResult()->message(), 'bad', false);
|
||||
$responseNegotiator = new PjaxResponseNegotiator(array(
|
||||
'CurrentForm' => function() use(&$form) {
|
||||
return $form->forTemplate();
|
||||
},
|
||||
'default' => function() use(&$controller) {
|
||||
return $controller->redirectBack();
|
||||
}
|
||||
));
|
||||
if($controller->getRequest()->isAjax()){
|
||||
$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
|
||||
}
|
||||
return $responseNegotiator->respond($controller->getRequest());
|
||||
$this->saveFormIntoRecord($data, $form);
|
||||
} catch (ValidationException $e) {
|
||||
return $this->generateValidationResponse($form, $e);
|
||||
}
|
||||
|
||||
// TODO Save this item into the given relationship
|
||||
|
||||
$link = '<a href="' . $this->Link('edit') . '">"'
|
||||
. htmlspecialchars($this->record->Title, ENT_QUOTES)
|
||||
. '"</a>';
|
||||
@ -580,7 +588,19 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
|
||||
$form->sessionMessage($message, 'good', false);
|
||||
|
||||
if($new_record) {
|
||||
// Redirect after save
|
||||
return $this->redirectAfterSave($isNewRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Response object for this request after a successful save
|
||||
*
|
||||
* @param bool $isNewRecord True if this record was just created
|
||||
* @return SS_HTTPResponse|HTMLText
|
||||
*/
|
||||
protected function redirectAfterSave($isNewRecord) {
|
||||
$controller = $this->getToplevelController();
|
||||
if($isNewRecord) {
|
||||
return $controller->redirect($this->Link());
|
||||
} elseif($this->gridField->getList()->byId($this->record->ID)) {
|
||||
// Return new view, as we can't do a "virtual redirect" via the CMS Ajax
|
||||
@ -596,6 +616,69 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public function httpError($errorCode, $errorMessage = null) {
|
||||
$controller = $this->getToplevelController();
|
||||
return $controller->httpError($errorCode, $errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given form data into the underlying dataobject and relation
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @throws ValidationException On error
|
||||
* @return DataObject Saved record
|
||||
*/
|
||||
protected function saveFormIntoRecord($data, $form) {
|
||||
$list = $this->gridField->getList();
|
||||
|
||||
// Check object matches the correct classname
|
||||
if (isset($data['ClassName']) && $data['ClassName'] != $this->record->ClassName) {
|
||||
$newClassName = $data['ClassName'];
|
||||
// The records originally saved attribute was overwritten by $form->saveInto($record) before.
|
||||
// This is necessary for newClassInstance() to work as expected, and trigger change detection
|
||||
// on the ClassName attribute
|
||||
$this->record->setClassName($this->record->ClassName);
|
||||
// Replace $record with a new instance
|
||||
$this->record = $this->record->newClassInstance($newClassName);
|
||||
}
|
||||
|
||||
// Save form and any extra saved data into this dataobject
|
||||
$form->saveInto($this->record);
|
||||
$this->record->write();
|
||||
$extraData = $this->getExtraSavedData($this->record, $list);
|
||||
$list->add($this->record, $extraData);
|
||||
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response object for a form validation error
|
||||
*
|
||||
* @param Form $form The source form
|
||||
* @param ValidationException $e The validation error message
|
||||
* @return SS_HTTPResponse
|
||||
* @throws SS_HTTPResponse_Exception
|
||||
*/
|
||||
protected function generateValidationResponse($form, $e) {
|
||||
$controller = $this->getToplevelController();
|
||||
|
||||
$form->sessionMessage($e->getResult()->message(), 'bad', false);
|
||||
$responseNegotiator = new PjaxResponseNegotiator(array(
|
||||
'CurrentForm' => function() use(&$form) {
|
||||
return $form->forTemplate();
|
||||
},
|
||||
'default' => function() use(&$controller) {
|
||||
return $controller->redirectBack();
|
||||
}
|
||||
));
|
||||
if($controller->getRequest()->isAjax()){
|
||||
$controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
|
||||
}
|
||||
return $responseNegotiator->respond($controller->getRequest());
|
||||
}
|
||||
|
||||
|
||||
public function doDelete($data, $form) {
|
||||
$title = $this->record->Title;
|
||||
try {
|
||||
|
@ -2,6 +2,8 @@
|
||||
/**
|
||||
* An extension that adds additional functionality to a {@link DataObject}.
|
||||
*
|
||||
* @property DataObject $owner
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
// namespace SilverStripe\Framework\Model\Versioning
|
||||
|
||||
/**
|
||||
* The Versioned extension allows your DataObjects to have several versions,
|
||||
* allowing you to rollback changes and view history. An example of this is
|
||||
@ -739,13 +741,116 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* If a write was skipped, then we need to ensure that we don't leave a
|
||||
* migrateVersion() value lying around for the next write.
|
||||
*
|
||||
*
|
||||
*/
|
||||
public function onAfterSkippedWrite() {
|
||||
$this->migrateVersion(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should return true if the current user can publish this record.
|
||||
* It can be overloaded to customise the security model for an application.
|
||||
*
|
||||
* Denies permission if any of the following conditions is true:
|
||||
* - canPublish() on any extension returns false
|
||||
* - canEdit() returns false
|
||||
*
|
||||
* @param Member $member
|
||||
* @return bool True if the current user can publish this record.
|
||||
*/
|
||||
public function canPublish($member = null) {
|
||||
// Skip if invoked by extendedCan()
|
||||
if(func_num_args() > 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!$member) {
|
||||
$member = Member::currentUser();
|
||||
}
|
||||
|
||||
if(Permission::checkMember($member, "ADMIN")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard mechanism for accepting permission changes from extensions
|
||||
$extended = $this->owner->extendedCan('canPublish', $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
|
||||
// Default to relying on edit permission
|
||||
return $this->owner->canEdit($member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user can delete this record from live
|
||||
*
|
||||
* @param null $member
|
||||
* @return mixed
|
||||
*/
|
||||
public function canUnpublish($member = null) {
|
||||
// Skip if invoked by extendedCan()
|
||||
if(func_num_args() > 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!$member) {
|
||||
$member = Member::currentUser();
|
||||
}
|
||||
|
||||
if(Permission::checkMember($member, "ADMIN")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard mechanism for accepting permission changes from extensions
|
||||
$extended = $this->owner->extendedCan('canUnpublish', $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
|
||||
// Default to relying on canPublish
|
||||
return $this->owner->canPublish($member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is allowed to archive this record.
|
||||
* If extended, ensure that both canDelete and canUnpublish are extended also
|
||||
*
|
||||
* @param Member $member
|
||||
* @return bool
|
||||
*/
|
||||
public function canArchive($member = null) {
|
||||
// Skip if invoked by extendedCan()
|
||||
if(func_num_args() > 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!$member) {
|
||||
$member = Member::currentUser();
|
||||
}
|
||||
|
||||
if(Permission::checkMember($member, "ADMIN")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard mechanism for accepting permission changes from extensions
|
||||
$extended = $this->owner->extendedCan('canArchive', $member);
|
||||
if($extended !== null) {
|
||||
return $extended;
|
||||
}
|
||||
|
||||
// Check if this record can be deleted from stage
|
||||
if(!$this->owner->canDelete($member)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we can delete from live
|
||||
if(!$this->owner->canUnpublish($member)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend permissions to include additional security for objects that are not published to live.
|
||||
*
|
||||
@ -788,6 +893,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bypass if record doesn't have a live stage
|
||||
if(!in_array(static::get_live_stage(), $this->getVersionedStages())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we weren't definitely loaded from live, and we can't view non-live content, we need to
|
||||
// check to make sure this version is the live version and so can be viewed
|
||||
$latestVersion = Versioned::get_versionnumber_by_stage($this->owner->class, 'Live', $this->owner->ID);
|
||||
@ -900,6 +1010,77 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
)->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a simple doPublish action for Versioned dataobjects
|
||||
*
|
||||
* @return bool True if publish was successful
|
||||
*/
|
||||
public function doPublish() {
|
||||
$owner = $this->owner;
|
||||
$owner->invokeWithExtensions('onBeforePublish');
|
||||
$owner->write();
|
||||
$owner->publish("Stage", "Live");
|
||||
$owner->invokeWithExtensions('onAfterPublish');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Removes the record from both live and stage
|
||||
*
|
||||
* @return bool Success
|
||||
*/
|
||||
public function doArchive() {
|
||||
$owner = $this->owner;
|
||||
$owner->invokeWithExtensions('onBeforeArchive', $this);
|
||||
|
||||
if($owner->doUnpublish()) {
|
||||
$owner->delete();
|
||||
$owner->invokeWithExtensions('onAfterArchive', $this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this record from the live site
|
||||
*
|
||||
* @return bool Flag whether the unpublish was successful
|
||||
*
|
||||
* @uses SiteTreeExtension->onBeforeUnpublish()
|
||||
* @uses SiteTreeExtension->onAfterUnpublish()
|
||||
*/
|
||||
public function doUnpublish() {
|
||||
$owner = $this->owner;
|
||||
if(!$owner->isInDB()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$owner->invokeWithExtensions('onBeforeUnpublish');
|
||||
|
||||
$origStage = self::current_stage();
|
||||
self::reading_stage(self::get_live_stage());
|
||||
|
||||
// This way our ID won't be unset
|
||||
$clone = clone $owner;
|
||||
$clone->delete();
|
||||
|
||||
self::reading_stage($origStage);
|
||||
|
||||
// If we're on the draft site, then we can update the status.
|
||||
// Otherwise, these lines will resurrect an inappropriate record
|
||||
if(self::current_stage() != self::get_live_stage() && $this->isOnDraft()) {
|
||||
$owner->write();
|
||||
}
|
||||
|
||||
$owner->invokeWithExtensions('onAfterUnpublish');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a database record from one stage to the other.
|
||||
*
|
||||
@ -909,50 +1090,55 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* By default, the existing version number will be copied over.
|
||||
*/
|
||||
public function publish($fromStage, $toStage, $createNewVersion = false) {
|
||||
$this->owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
$owner = $this->owner;
|
||||
$owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
|
||||
$baseClass = $this->owner->class;
|
||||
$baseClass = $owner->class;
|
||||
while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p;
|
||||
$extTable = $this->extendWithSuffix($baseClass);
|
||||
|
||||
if(is_numeric($fromStage)) {
|
||||
$from = Versioned::get_version($baseClass, $this->owner->ID, $fromStage);
|
||||
$from = Versioned::get_version($baseClass, $owner->ID, $fromStage);
|
||||
} else {
|
||||
$this->owner->flushCache();
|
||||
$from = Versioned::get_one_by_stage($baseClass, $fromStage, "\"{$baseClass}\".\"ID\"={$this->owner->ID}");
|
||||
$owner->flushCache();
|
||||
$from = Versioned::get_one_by_stage($baseClass, $fromStage, "\"{$baseClass}\".\"ID\"={$owner->ID}");
|
||||
}
|
||||
if(!$from) {
|
||||
throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
|
||||
}
|
||||
|
||||
$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
|
||||
if($from) {
|
||||
$from->forceChange();
|
||||
if($createNewVersion) {
|
||||
$latest = self::get_latest_version($baseClass, $this->owner->ID);
|
||||
$this->owner->Version = $latest->Version + 1;
|
||||
} else {
|
||||
$from->migrateVersion($from->Version);
|
||||
}
|
||||
|
||||
// Mark this version as having been published at some stage
|
||||
DB::prepared_query("UPDATE \"{$extTable}_versions\"
|
||||
SET \"WasPublished\" = ?, \"PublisherID\" = ?
|
||||
WHERE \"RecordID\" = ? AND \"Version\" = ?",
|
||||
array(1, $publisherID, $from->ID, $from->Version)
|
||||
);
|
||||
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($toStage);
|
||||
|
||||
$conn = DB::get_conn();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, true);
|
||||
$from->write();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, false);
|
||||
|
||||
$from->destroy();
|
||||
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
$from->forceChange();
|
||||
if($createNewVersion) {
|
||||
$latest = self::get_latest_version($baseClass, $owner->ID);
|
||||
$owner->Version = $latest->Version + 1;
|
||||
} else {
|
||||
user_error("Can't find {$this->owner->URLSegment}/{$this->owner->ID} in stage $fromStage", E_USER_WARNING);
|
||||
$from->migrateVersion($from->Version);
|
||||
}
|
||||
|
||||
// Mark this version as having been published at some stage
|
||||
DB::prepared_query("UPDATE \"{$extTable}_versions\"
|
||||
SET \"WasPublished\" = ?, \"PublisherID\" = ?
|
||||
WHERE \"RecordID\" = ? AND \"Version\" = ?",
|
||||
array(1, $publisherID, $from->ID, $from->Version)
|
||||
);
|
||||
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($toStage);
|
||||
|
||||
$conn = DB::get_conn();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, true);
|
||||
// Migrate stage prior to write
|
||||
$from->setSourceQueryParam('Versioned.mode', 'stage');
|
||||
$from->setSourceQueryParam('Versioned.stage', $toStage);
|
||||
$from->write();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, false);
|
||||
|
||||
$from->destroy();
|
||||
|
||||
Versioned::set_reading_mode($oldMode);
|
||||
|
||||
$owner->extend('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1274,7 +1460,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
return self::$cache_versionnumber[$baseClass][$stage][$id];
|
||||
}
|
||||
|
||||
// get version as performance-optimized SQL query (gets called for each page in the sitetree)
|
||||
// get version as performance-optimized SQL query (gets called for each record in the sitetree)
|
||||
$version = DB::prepared_query(
|
||||
"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
|
||||
array($id)
|
||||
@ -1299,7 +1485,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
|
||||
* a list of record IDs, for more efficient database querying. If $idList
|
||||
* is null, then every page will be pre-cached.
|
||||
* is null, then every record will be pre-cached.
|
||||
*
|
||||
* @param string $class
|
||||
* @param string $stage
|
||||
@ -1391,22 +1577,21 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll the draft version of this page to match the published page.
|
||||
* Roll the draft version of this record to match the published record.
|
||||
* Caution: Doesn't overwrite the object properties with the rolled back version.
|
||||
*
|
||||
* @param int $version Either the string 'Live' or a version number
|
||||
*/
|
||||
public function doRollbackTo($version) {
|
||||
$this->owner->extend('onBeforeRollback', $version);
|
||||
$owner = $this->owner;
|
||||
$owner->extend('onBeforeRollback', $version);
|
||||
$this->publish($version, "Stage", true);
|
||||
|
||||
$this->owner->writeWithoutVersion();
|
||||
|
||||
$this->owner->extend('onAfterRollback', $version);
|
||||
$owner->writeWithoutVersion();
|
||||
$owner->extend('onAfterRollback', $version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest version of the given page.
|
||||
* Return the latest version of the given record.
|
||||
*
|
||||
* @return DataObject
|
||||
*/
|
||||
@ -1430,14 +1615,55 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLatestVersion() {
|
||||
$version = self::get_latest_version($this->owner->class, $this->owner->ID);
|
||||
if(!$this->owner->isInDB()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$version = self::get_latest_version($this->owner->class, $this->owner->ID);
|
||||
return ($version->Version == $this->owner->Version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this record exists on live
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPublished() {
|
||||
if(!$this->owner->isInDB()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$table = ClassInfo::baseDataClass($this->owner->class) . '_' . self::get_live_stage();
|
||||
$result = DB::prepared_query(
|
||||
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
||||
array($this->owner->ID)
|
||||
);
|
||||
return (bool)$result->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this record exists on the draft stage
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isOnDraft() {
|
||||
if(!$this->owner->isInDB()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$table = ClassInfo::baseDataClass($this->owner->class);
|
||||
$result = DB::prepared_query(
|
||||
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
||||
array($this->owner->ID)
|
||||
);
|
||||
return (bool)$result->value();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return the equivalent of a DataList::create() call, querying the latest
|
||||
* version of each page stored in the (class)_versions tables.
|
||||
* version of each record stored in the (class)_versions tables.
|
||||
*
|
||||
* In particular, this will query deleted records as well as active ones.
|
||||
*
|
||||
@ -1498,7 +1724,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @param array $labels
|
||||
*/
|
||||
public function updateFieldLabels(&$labels) {
|
||||
$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this page');
|
||||
$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
|
||||
}
|
||||
|
||||
/**
|
13
model/versioning/VersionedGridFieldDetailForm.php
Normal file
13
model/versioning/VersionedGridFieldDetailForm.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Extends {@see GridFieldDetailForm}
|
||||
*/
|
||||
class VersionedGridFieldDetailForm extends Extension {
|
||||
public function updateItemRequestClass(&$class, $gridField, $record, $requestHandler) {
|
||||
// Conditionally use a versioned item handler
|
||||
if($record && $record->has_extension('Versioned')) {
|
||||
$class = 'VersionedGridFieldItemRequest';
|
||||
}
|
||||
}
|
||||
}
|
196
model/versioning/VersionedGridFieldItemRequest.php
Normal file
196
model/versioning/VersionedGridFieldItemRequest.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Provides versioned dataobject support to {@see GridFieldDetailForm_ItemRequest}
|
||||
*
|
||||
* @property GridFieldDetailForm_ItemRequest $owner
|
||||
*/
|
||||
class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest {
|
||||
|
||||
protected function getFormActions() {
|
||||
$actions = parent::getFormActions();
|
||||
|
||||
// Check if record is versionable
|
||||
$record = $this->getRecord();
|
||||
if(!$record || !$record->has_extension('Versioned')) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
// Save & Publish action
|
||||
if($record->canPublish()) {
|
||||
// "publish", as with "save", it supports an alternate state to show when action is needed.
|
||||
$publish = FormAction::create(
|
||||
'doPublish',
|
||||
_t('VersionedGridFieldItemRequest.BUTTONPUBLISH', 'Publish')
|
||||
)
|
||||
->setUseButtonTag(true)
|
||||
->addExtraClass('ss-ui-action-constructive')
|
||||
->setAttribute('data-icon', 'accept');
|
||||
|
||||
// Insert after save
|
||||
if($actions->fieldByName('action_doSave')) {
|
||||
$actions->insertAfter('action_doSave', $publish);
|
||||
} else {
|
||||
$actions->push($publish);
|
||||
}
|
||||
}
|
||||
|
||||
// Unpublish action
|
||||
$isPublished = $record->isPublished();
|
||||
if($isPublished && $record->canUnpublish()) {
|
||||
$actions->push(
|
||||
FormAction::create(
|
||||
'doUnpublish',
|
||||
_t('VersionedGridFieldItemRequest.BUTTONUNPUBLISH', 'Unpublish')
|
||||
)
|
||||
->setUseButtonTag(true)
|
||||
->setDescription(_t(
|
||||
'VersionedGridFieldItemRequest.BUTTONUNPUBLISHDESC',
|
||||
'Remove this record from the published site'
|
||||
))
|
||||
->addExtraClass('ss-ui-action-destructive')
|
||||
);
|
||||
}
|
||||
|
||||
// Archive action
|
||||
if($record->canArchive()) {
|
||||
// Replace "delete" action
|
||||
$actions->removeByName('action_doDelete');
|
||||
|
||||
// "archive"
|
||||
$actions->push(
|
||||
FormAction::create('doArchive', _t('VersionedGridFieldItemRequest.ARCHIVE','Archive'))
|
||||
->setDescription(_t(
|
||||
'VersionedGridFieldItemRequest.BUTTONARCHIVEDESC',
|
||||
'Unpublish and send to archive'
|
||||
))
|
||||
->addExtraClass('delete ss-ui-action-destructive')
|
||||
);
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive this versioned record
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function doArchive($data, $form) {
|
||||
$record = $this->getRecord();
|
||||
if (!$record->canArchive()) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
|
||||
// Record name before it's deleted
|
||||
$title = $record->Title;
|
||||
|
||||
try {
|
||||
$record->doArchive();
|
||||
} catch(ValidationException $e) {
|
||||
return $this->generateValidationResponse($form, $e);
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
_t('VersionedGridFieldItemRequest.Archived', 'Archived %s %s'),
|
||||
$record->i18n_singular_name(),
|
||||
Convert::raw2xml($title)
|
||||
);
|
||||
$this->setFormMessage($form, $message);
|
||||
|
||||
//when an item is deleted, redirect to the parent controller
|
||||
$controller = $this->getToplevelController();
|
||||
$controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
|
||||
|
||||
return $controller->redirect($this->getBacklink(), 302); //redirect back to admin section
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish this versioned record
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function doPublish($data, $form) {
|
||||
$record = $this->getRecord();
|
||||
$isNewRecord = $record->ID == 0;
|
||||
|
||||
// Check permission
|
||||
if(!$record->canPublish()) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
|
||||
// Save from form data
|
||||
try {
|
||||
// Initial save and reload
|
||||
$record = $this->saveFormIntoRecord($data, $form);
|
||||
$record->doPublish();
|
||||
|
||||
} catch(ValidationException $e) {
|
||||
return $this->generateValidationResponse($form, $e);
|
||||
}
|
||||
|
||||
$editURL = $this->Link('edit');
|
||||
$xmlTitle = Convert::raw2xml($record->Title);
|
||||
$link = "<a href=\"{$editURL}\">{$xmlTitle}</a>";
|
||||
$message = _t(
|
||||
'VersionedGridFieldItemRequest.Published',
|
||||
'Published {name} {link}',
|
||||
array(
|
||||
'name' => $record->i18n_singular_name(),
|
||||
'link' => $link
|
||||
)
|
||||
);
|
||||
$this->setFormMessage($form, $message);
|
||||
|
||||
return $this->redirectAfterSave($isNewRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete this record from the live site
|
||||
*
|
||||
* @param array $data
|
||||
* @param Form $form
|
||||
* @return SS_HTTPResponse
|
||||
*/
|
||||
public function doUnpublish($data, $form) {
|
||||
$record = $this->getRecord();
|
||||
if (!$record->canUnpublish()) {
|
||||
return $this->httpError(403);
|
||||
}
|
||||
|
||||
// Record name before it's deleted
|
||||
$title = $record->Title;
|
||||
|
||||
try {
|
||||
$record->doUnpublish();
|
||||
} catch(ValidationException $e) {
|
||||
return $this->generateValidationResponse($form, $e);
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
_t('VersionedGridFieldItemRequest.Unpublished', 'Unpublished %s %s'),
|
||||
$record->i18n_singular_name(),
|
||||
Convert::raw2xml($title)
|
||||
);
|
||||
$this->setFormMessage($form, $message);
|
||||
|
||||
// Redirect back to edit
|
||||
return $this->redirectAfterSave(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Form $form
|
||||
* @param string $message
|
||||
*/
|
||||
protected function setFormMessage($form, $message) {
|
||||
$form->sessionMessage($message, 'good', false);
|
||||
$controller = $this->getToplevelController();
|
||||
if($controller->hasMethod('getEditForm')) {
|
||||
$backForm = $controller->getEditForm();
|
||||
$backForm->sessionMessage($message, 'good', false);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,13 @@
|
||||
*/
|
||||
interface ShortcodeHandler {
|
||||
|
||||
/**
|
||||
* Gets the list of shortcodes provided by this handler
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_shortcodes();
|
||||
|
||||
/**
|
||||
* Generate content with a shortcode value
|
||||
*
|
||||
|
@ -17,6 +17,8 @@ class MigrateFileTask extends BuildTask {
|
||||
$migrated = FileMigrationHelper::singleton()->run();
|
||||
if($migrated) {
|
||||
DB::alteration_message("{$migrated} File DataObjects upgraded", "changed");
|
||||
} else {
|
||||
DB::alteration_message("No File DataObjects need upgrading", "notice");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,9 @@ class AssetControlExtensionTest extends SapphireTest {
|
||||
parent::setUp();
|
||||
|
||||
// Set backend and base url
|
||||
\Versioned::reading_stage('Stage');
|
||||
AssetStoreTest_SpyStore::activate('AssetControlExtensionTest');
|
||||
$this->logInWithPermission('ADMIN');
|
||||
|
||||
// Setup fixture manually
|
||||
$object1 = new AssetControlExtensionTest_VersionedObject();
|
||||
@ -24,7 +26,7 @@ class AssetControlExtensionTest extends SapphireTest {
|
||||
$object1->Header->setFromLocalFile($fish1, 'Header/MyObjectHeader.jpg');
|
||||
$object1->Download->setFromString('file content', 'Documents/File.txt');
|
||||
$object1->write();
|
||||
$object1->publish('Stage', 'Live');
|
||||
$object1->doPublish();
|
||||
|
||||
$object2 = new AssetControlExtensionTest_Object();
|
||||
$object2->Title = 'Unversioned';
|
||||
@ -35,7 +37,7 @@ class AssetControlExtensionTest extends SapphireTest {
|
||||
$object3->Title = 'Archived';
|
||||
$object3->Header->setFromLocalFile($fish1, 'Archived/MyObjectHeader.jpg');
|
||||
$object3->write();
|
||||
$object3->publish('Stage', 'Live');
|
||||
$object3->doPublish();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
@ -44,6 +46,8 @@ class AssetControlExtensionTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testFileDelete() {
|
||||
\Versioned::reading_stage('Stage');
|
||||
|
||||
/** @var AssetControlExtensionTest_VersionedObject $object1 */
|
||||
$object1 = AssetControlExtensionTest_VersionedObject::get()
|
||||
->filter('Title', 'My object')
|
||||
@ -111,6 +115,87 @@ class AssetControlExtensionTest extends SapphireTest {
|
||||
$this->assertNull($object2->Image->getVisibility());
|
||||
$this->assertEquals(AssetStore::VISIBILITY_PROTECTED, $object3->Header->getVisibility());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test files being replaced
|
||||
*/
|
||||
public function testReplaceFile() {
|
||||
\Versioned::reading_stage('Stage');
|
||||
|
||||
/** @var AssetControlExtensionTest_VersionedObject $object1 */
|
||||
$object1 = AssetControlExtensionTest_VersionedObject::get()
|
||||
->filter('Title', 'My object')
|
||||
->first();
|
||||
/** @var AssetControlExtensionTest_Object $object2 */
|
||||
$object2 = AssetControlExtensionTest_Object::get()
|
||||
->filter('Title', 'Unversioned')
|
||||
->first();
|
||||
|
||||
/** @var AssetControlExtensionTest_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__ .'/../model/testimages/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->doPublish();
|
||||
$object3->doPublish();
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,6 +216,25 @@ class AssetControlExtensionTest_VersionedObject extends DataObject implements Te
|
||||
'Header' => "DBFile('image/supported')",
|
||||
'Download' => 'DBFile'
|
||||
);
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,11 +248,20 @@ class AssetControlExtensionTest_Object extends DataObject implements TestOnly {
|
||||
'Title' => 'Varchar(255)',
|
||||
'Image' => "DBFile('image/supported')"
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* @param Member $member
|
||||
* @return bool
|
||||
*/
|
||||
public function canView($member = null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versioned object that always archives its assets
|
||||
*/
|
||||
class AssetControlExtensionTest_ArchivedObject extends AssetControlExtensionTest_VersionedObject {
|
||||
private static $archive_assets = true;
|
||||
private static $keep_archived_assets = true;
|
||||
}
|
||||
|
79
tests/filesystem/AssetManipulationListTest.php
Normal file
79
tests/filesystem/AssetManipulationListTest.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
use SilverStripe\Filesystem\AssetManipulationList;
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ class FileMigrationHelperTest extends SapphireTest {
|
||||
$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
|
||||
@ -77,6 +78,7 @@ class FileMigrationHelperTest extends SapphireTest {
|
||||
"File with name {$filename} has the correct hash"
|
||||
);
|
||||
$this->assertTrue($file->exists(), "File with name {$filename} exists");
|
||||
$this->assertTrue($file->isPublished(), "File is published after migration");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Filesystem as SS_Filesystem;
|
||||
use SilverStripe\Filesystem\Storage\AssetStore;
|
||||
|
||||
/**
|
||||
* Tests for the File class
|
||||
@ -13,6 +14,8 @@ class FileTest extends SapphireTest {
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$this->logInWithPermission('ADMIN');
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Set backend root to /ImageTest
|
||||
AssetStoreTest_SpyStore::activate('FileTest');
|
||||
@ -232,40 +235,92 @@ class FileTest extends SapphireTest {
|
||||
|
||||
public function testSetNameChangesFilesystemOnWrite() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$oldPath = AssetStoreTest_SpyStore::getLocalPath($file);
|
||||
$newPath = str_replace('FileTest.txt', 'renamed.txt', $oldPath);
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$file->doPublish();
|
||||
$oldTuple = $file->File->getValue();
|
||||
|
||||
// Rename
|
||||
$file->Name = 'renamed.txt';
|
||||
$newTuple = $oldTuple;
|
||||
$newTuple['Filename'] = $file->getFilename();
|
||||
|
||||
// Before write()
|
||||
$file->Name = 'renamed.txt';
|
||||
$this->assertFileExists($oldPath, 'Old path is still present');
|
||||
$this->assertFileNotExists($newPath, 'New path is updated in memory, not written before write() is called');
|
||||
$file->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()
|
||||
$this->assertFileExists($oldPath, 'Old path is left after write()');
|
||||
$this->assertFileExists($newPath, 'New path is created 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->doPublish();
|
||||
$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', 'asdf');
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$file->doPublish();
|
||||
$subfolder = $this->objFromFixture('Folder', 'subfolder');
|
||||
$oldPath = AssetStoreTest_SpyStore::getLocalPath($file);
|
||||
$newPath = str_replace('assets/FileTest/', 'assets/FileTest/FileTest-subfolder/', $oldPath);
|
||||
$oldTuple = $file->File->getValue();
|
||||
|
||||
// set ParentID
|
||||
$file->ParentID = $subfolder->ID;
|
||||
$newTuple = $oldTuple;
|
||||
$newTuple['Filename'] = $file->getFilename();
|
||||
|
||||
// Before write()
|
||||
$this->assertFileExists($oldPath, 'Old path is still present');
|
||||
$this->assertFileNotExists($newPath, 'New path is updated in memory, not written before write() is called');
|
||||
$this->assertEquals($oldPath, AssetStoreTest_SpyStore::getLocalPath($file), 'URL is not updated until write is called');
|
||||
|
||||
$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()
|
||||
$this->assertFileExists($oldPath, 'Old path is left after write()');
|
||||
$this->assertFileExists($newPath, 'New path is created after write()');
|
||||
$this->assertEquals($newPath, AssetStoreTest_SpyStore::getLocalPath($file), 'URL is updated after write is called');
|
||||
$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->doPublish();
|
||||
$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()'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -354,13 +409,29 @@ class FileTest extends SapphireTest {
|
||||
|
||||
public function testDeleteFile() {
|
||||
$file = $this->objFromFixture('File', 'asdf');
|
||||
$fileID = $file->ID;
|
||||
$filePath = AssetStoreTest_SpyStore::getLocalPath($file);
|
||||
$file->delete();
|
||||
$this->logInWithPermission('ADMIN');
|
||||
$file->doPublish();
|
||||
$tuple = $file->File->getValue();
|
||||
|
||||
// File is deleted
|
||||
$this->assertFileNotExists($filePath);
|
||||
$this->assertEmpty(DataObject::get_by_id('File', $fileID));
|
||||
// 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() {
|
||||
@ -462,6 +533,13 @@ class FileTest extends SapphireTest {
|
||||
$this->assertEquals('', File::join_paths('/', '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AssetStore
|
||||
*/
|
||||
protected function getAssetStore() {
|
||||
return Injector::inst()->get('AssetStore');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FileTest_MyCustomFile extends File implements TestOnly {
|
||||
|
@ -15,6 +15,9 @@ class FolderTest extends SapphireTest {
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Set backend root to /FolderTest
|
||||
AssetStoreTest_SpyStore::activate('FolderTest');
|
||||
|
||||
@ -123,7 +126,7 @@ class FolderTest extends SapphireTest {
|
||||
|
||||
// File should be located in new folder
|
||||
$this->assertEquals(
|
||||
ASSETS_PATH . '/FolderTest/FileTest-folder2/FileTest-folder1/55b443b601/File1.txt',
|
||||
ASSETS_PATH . '/FolderTest/.protected/FileTest-folder2/FileTest-folder1/55b443b601/File1.txt',
|
||||
AssetStoreTest_SpyStore::getLocalPath($file1)
|
||||
);
|
||||
}
|
||||
@ -153,7 +156,7 @@ class FolderTest extends SapphireTest {
|
||||
|
||||
// File should be located in new folder
|
||||
$this->assertEquals(
|
||||
ASSETS_PATH . '/FolderTest/FileTest-folder1-changed/55b443b601/File1.txt',
|
||||
ASSETS_PATH . '/FolderTest/.protected/FileTest-folder1-changed/55b443b601/File1.txt',
|
||||
AssetStoreTest_SpyStore::getLocalPath($file1)
|
||||
);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ class UploadTest extends SapphireTest {
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
Versioned::reading_stage('Stage');
|
||||
AssetStoreTest_SpyStore::activate('UploadTest');
|
||||
}
|
||||
|
||||
@ -48,7 +49,7 @@ class UploadTest extends SapphireTest {
|
||||
$file1->getFilename()
|
||||
);
|
||||
$this->assertEquals(
|
||||
BASE_PATH . '/assets/UploadTest/Uploads/315ae4c3d4/UploadTest-testUpload.txt',
|
||||
BASE_PATH . '/assets/UploadTest/.protected/Uploads/315ae4c3d4/UploadTest-testUpload.txt',
|
||||
AssetStoreTest_SpyStore::getLocalPath($file1)
|
||||
);
|
||||
$this->assertFileExists(
|
||||
@ -66,7 +67,7 @@ class UploadTest extends SapphireTest {
|
||||
$file2->getFilename()
|
||||
);
|
||||
$this->assertEquals(
|
||||
BASE_PATH . '/assets/UploadTest/UploadTest-testUpload/315ae4c3d4/UploadTest-testUpload.txt',
|
||||
BASE_PATH . '/assets/UploadTest/.protected/UploadTest-testUpload/315ae4c3d4/UploadTest-testUpload.txt',
|
||||
AssetStoreTest_SpyStore::getLocalPath($file2)
|
||||
);
|
||||
$this->assertFileExists(
|
||||
|
@ -15,6 +15,9 @@ class AssetFieldTest extends FunctionalTest {
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->logInWithPermission('ADMIN');
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Set backend root to /AssetFieldTest
|
||||
AssetStoreTest_SpyStore::activate('AssetFieldTest');
|
||||
$create = function($path) {
|
||||
@ -57,7 +60,7 @@ class AssetFieldTest extends FunctionalTest {
|
||||
$this->assertEquals('315ae4c3d44412baa0c81515b6fb35829a337a5a', $responseJSON[0]['hash']);
|
||||
$this->assertEmpty($responseJSON[0]['variant']);
|
||||
$this->assertFileExists(
|
||||
BASE_PATH . '/assets/AssetFieldTest/MyDocuments/315ae4c3d4/testUploadBasic.txt'
|
||||
BASE_PATH . '/assets/AssetFieldTest/.protected/MyDocuments/315ae4c3d4/testUploadBasic.txt'
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,7 +82,7 @@ class AssetFieldTest extends FunctionalTest {
|
||||
$responseJSON = json_decode($response->getBody(), true);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(
|
||||
BASE_PATH . '/assets/AssetFieldTest/MyFiles/315ae4c3d4/testUploadHasOneRelation.txt'
|
||||
BASE_PATH . '/assets/AssetFieldTest/.protected/MyFiles/315ae4c3d4/testUploadHasOneRelation.txt'
|
||||
);
|
||||
|
||||
// Secondly, ensure that simply uploading an object does not save the file against the relation
|
||||
@ -148,8 +151,7 @@ class AssetFieldTest extends FunctionalTest {
|
||||
$this->assertFalse($record->File->exists());
|
||||
|
||||
// Check file object itself exists
|
||||
// @todo - When assets are removed from a DBFile reference, these files should be archived
|
||||
$this->assertFileExists($filePath, 'File is only detached, not deleted from filesystem');
|
||||
$this->assertFileNotExists($filePath, 'File is deleted once detached');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,6 +250,7 @@ class AssetFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testCanUploadWithPermissionCode() {
|
||||
Session::clear("loggedInAs");
|
||||
$field = AssetField::create('MyField');
|
||||
|
||||
$field->setCanUpload(true);
|
||||
|
@ -14,9 +14,17 @@ class UploadFieldTest extends FunctionalTest {
|
||||
'File' => array('UploadFieldTest_FileExtension')
|
||||
);
|
||||
|
||||
protected $oldReadingMode = null;
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Save versioned state
|
||||
$this->oldReadingMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage('Stage');
|
||||
|
||||
// Set backend root to /UploadFieldTest
|
||||
AssetStoreTest_SpyStore::activate('UploadFieldTest');
|
||||
|
||||
@ -39,6 +47,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
|
||||
public function tearDown() {
|
||||
AssetStoreTest_SpyStore::reset();
|
||||
if($this->oldReadingMode) {
|
||||
Versioned::set_reading_mode($this->oldReadingMode);
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
@ -46,14 +57,13 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that files can be uploaded against an object with no relation
|
||||
*/
|
||||
public function testUploadNoRelation() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$tmpFileName = 'testUploadBasic.txt';
|
||||
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$uploadedFile = DataObject::get_one('File', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
|
||||
$this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
}
|
||||
@ -62,8 +72,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that an object can be uploaded against an object with a has_one relation
|
||||
*/
|
||||
public function testUploadHasOneRelation() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Unset existing has_one relation before re-uploading
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$record->HasOneFileID = null;
|
||||
@ -95,8 +103,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Tests that has_one relations work with subclasses of File
|
||||
*/
|
||||
public function testUploadHasOneRelationWithExtendedFile() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Unset existing has_one relation before re-uploading
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$record->HasOneExtendedFileID = null;
|
||||
@ -129,8 +135,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that has_many relations work with files
|
||||
*/
|
||||
public function testUploadHasManyRelation() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
|
||||
// Test that uploaded files can be posted to a has_many relation
|
||||
@ -159,8 +163,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that many_many relationships work with files
|
||||
*/
|
||||
public function testUploadManyManyRelation() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$relationCount = $record->ManyManyFiles()->Count();
|
||||
|
||||
@ -195,8 +197,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* in this controller method.
|
||||
*/
|
||||
public function testAllowedExtensions() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Test invalid file
|
||||
// Relies on Upload_Validator failing to allow this extension
|
||||
$invalidFile = 'invalid.php';
|
||||
@ -237,8 +237,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that has_one relations do not support multiple files
|
||||
*/
|
||||
public function testAllowedMaxFileNumberWithHasOne() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Get references for each file to upload
|
||||
$file1 = $this->objFromFixture('File', 'file1');
|
||||
$file2 = $this->objFromFixture('File', 'file2');
|
||||
@ -272,8 +270,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that max number of items on has_many is validated
|
||||
*/
|
||||
public function testAllowedMaxFileNumberWithHasMany() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// The 'HasManyFilesMaxTwo' field has a maximum of two files able to be attached to it.
|
||||
// We want to add files to it until we attempt to add the third. We expect that the first
|
||||
// two should work and the third will fail.
|
||||
@ -407,8 +403,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that files can be deleted from has_one
|
||||
*/
|
||||
public function testDeleteFromHasOne() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file1 = $this->objFromFixture('File', 'file1');
|
||||
|
||||
@ -431,8 +425,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that files can be deleted from has_many
|
||||
*/
|
||||
public function testDeleteFromHasMany() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file2 = $this->objFromFixture('File', 'file2');
|
||||
$file3 = $this->objFromFixture('File', 'file3');
|
||||
@ -457,8 +449,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that files can be deleted from many_many and the filesystem
|
||||
*/
|
||||
public function testDeleteFromManyMany() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file4 = $this->objFromFixture('File', 'file4');
|
||||
$file5 = $this->objFromFixture('File', 'file5');
|
||||
@ -496,8 +486,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test control output html
|
||||
*/
|
||||
public function testView() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file4 = $this->objFromFixture('File', 'file4');
|
||||
$file5 = $this->objFromFixture('File', 'file5');
|
||||
@ -523,8 +511,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testEdit() {
|
||||
$memberID = $this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file4 = $this->objFromFixture('File', 'file4');
|
||||
$fileNoEdit = $this->objFromFixture('File', 'file-noedit');
|
||||
@ -630,8 +616,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testReadonly() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$response = $this->get('UploadFieldTest_Controller');
|
||||
$this->assertFalse($response->isError());
|
||||
|
||||
@ -655,8 +639,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testDisabled() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$response = $this->get('UploadFieldTest_Controller');
|
||||
$this->assertFalse($response->isError());
|
||||
|
||||
@ -677,7 +659,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testCanUpload() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
$response = $this->get('UploadFieldTest_Controller');
|
||||
$this->assertFalse($response->isError());
|
||||
|
||||
@ -697,6 +678,7 @@ class UploadFieldTest extends FunctionalTest {
|
||||
|
||||
public function testCanUploadWithPermissionCode() {
|
||||
$field = UploadField::create('MyField');
|
||||
Session::clear("loggedInAs");
|
||||
|
||||
$field->setCanUpload(true);
|
||||
$this->assertTrue($field->canUpload());
|
||||
@ -714,7 +696,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testCanAttachExisting() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
$response = $this->get('UploadFieldTest_Controller');
|
||||
$this->assertFalse($response->isError());
|
||||
|
||||
@ -740,8 +721,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testSelect() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file4 = $this->objFromFixture('File', 'file4');
|
||||
$fileSubfolder = $this->objFromFixture('File', 'file-subfolder');
|
||||
@ -758,8 +737,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
}
|
||||
|
||||
public function testSelectWithDisplayFolderName() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
$record = $this->objFromFixture('UploadFieldTest_Record', 'record1');
|
||||
$file4 = $this->objFromFixture('File', 'file4');
|
||||
$fileSubfolder = $this->objFromFixture('File', 'file-subfolder');
|
||||
@ -779,8 +756,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Test that UploadField:overwriteWarning cannot overwrite Upload:replaceFile
|
||||
*/
|
||||
public function testConfigOverwriteWarningCannotRelaceFiles() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
Upload::config()->replaceFile = false;
|
||||
UploadField::config()->defaultConfig = array_merge(
|
||||
UploadField::config()->defaultConfig, array('overwriteWarning' => true)
|
||||
@ -815,8 +790,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
* Tests that UploadField::fileexist works
|
||||
*/
|
||||
public function testFileExists() {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// Check that fileexist works on subfolders
|
||||
$nonFile = uniqid().'.txt';
|
||||
$responseEmpty = $this->mockFileExists('NoRelationField', $nonFile);
|
||||
@ -834,7 +807,7 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$tmpFileName = 'testUploadBasic.txt';
|
||||
$response = $this->mockFileUpload('RootFolderTest', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/315ae4c3d4/$tmpFileName");
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/.protected/315ae4c3d4/$tmpFileName");
|
||||
$responseExists = $this->mockFileExists('RootFolderTest', $tmpFileName);
|
||||
$responseExistsData = json_decode($responseExists->getBody());
|
||||
$this->assertFalse($responseExists->isError());
|
||||
@ -843,7 +816,7 @@ class UploadFieldTest extends FunctionalTest {
|
||||
// Check that uploaded files can be detected
|
||||
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/UploadedFiles/315ae4c3d4/$tmpFileName");
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/.protected/UploadedFiles/315ae4c3d4/$tmpFileName");
|
||||
$responseExists = $this->mockFileExists('NoRelationField', $tmpFileName);
|
||||
$responseExistsData = json_decode($responseExists->getBody());
|
||||
$this->assertFalse($responseExists->isError());
|
||||
@ -855,7 +828,7 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$tmpFileNameExpected = 'test-Upload-Bad.txt';
|
||||
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/UploadedFiles/315ae4c3d4/$tmpFileNameExpected");
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/.protected/UploadedFiles/315ae4c3d4/$tmpFileNameExpected");
|
||||
// With original file
|
||||
$responseExists = $this->mockFileExists('NoRelationField', $tmpFileName);
|
||||
$responseExistsData = json_decode($responseExists->getBody());
|
||||
@ -869,7 +842,6 @@ class UploadFieldTest extends FunctionalTest {
|
||||
|
||||
// Test that attempts to navigate outside of the directory return false
|
||||
$responseExists = $this->mockFileExists('NoRelationField', "../../../../var/private/$tmpFileName");
|
||||
$responseExistsData = json_decode($responseExists->getBody());
|
||||
$this->assertTrue($responseExists->isError());
|
||||
$this->assertContains('File is not a valid upload', $responseExists->getBody());
|
||||
}
|
||||
@ -922,6 +894,7 @@ class UploadFieldTest extends FunctionalTest {
|
||||
|
||||
$form = new UploadFieldTestForm();
|
||||
$form->loadDataFrom($data, true);
|
||||
|
||||
if($form->validate()) {
|
||||
$record = $form->getRecord();
|
||||
$form->saveInto($record);
|
||||
@ -996,6 +969,35 @@ class UploadFieldTest extends FunctionalTest {
|
||||
);
|
||||
}
|
||||
|
||||
public function get($url, $session = null, $headers = null, $cookies = null) {
|
||||
// Inject stage=Stage into the URL, to force working on draft
|
||||
$url = $this->addStageToUrl($url);
|
||||
return parent::get($url, $session, $headers, $cookies);
|
||||
}
|
||||
|
||||
public function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) {
|
||||
// Inject stage=Stage into the URL, to force working on draft
|
||||
$url = $this->addStageToUrl($url);
|
||||
return parent::post($url, $data, $headers, $session, $body, $cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ?stage=Stage to url
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function addStageToUrl($url) {
|
||||
if(stripos($url, 'stage=Stage') === false) {
|
||||
if(stripos($url, '?') === false) {
|
||||
$url .= '?stage=Stage';
|
||||
} else {
|
||||
$url .= '&stage=Stage';
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UploadFieldTest_Record extends DataObject implements TestOnly {
|
||||
|
@ -102,6 +102,22 @@ class VersionedTest extends SapphireTest {
|
||||
$this->assertEquals($count, $count2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that publishing from invalid stage will throw exception
|
||||
*/
|
||||
public function testInvalidPublish() {
|
||||
$obj = new VersionedTest_Subclass();
|
||||
$obj->ExtraField = 'Foo'; // ensure that child version table gets written
|
||||
$obj->write();
|
||||
$this->setExpectedException(
|
||||
'InvalidArgumentException',
|
||||
"Can't find VersionedTest_DataObject#{$obj->ID} in stage Live"
|
||||
);
|
||||
|
||||
// Fail publishing from live to stage
|
||||
$obj->publish('Live', 'Stage');
|
||||
}
|
||||
|
||||
public function testDuplicate() {
|
||||
$obj1 = new VersionedTest_Subclass();
|
||||
$obj1->ExtraField = 'Foo';
|
||||
|
Loading…
Reference in New Issue
Block a user