Merge pull request #4697 from open-sausages/features/dbfile-uploadfield

New AssetField for uploading to a DBFile database field
This commit is contained in:
Ingo Schommer 2015-10-23 17:20:55 +13:00
commit ecacf52863
23 changed files with 1650 additions and 117 deletions

View File

@ -1,14 +0,0 @@
name: uploadfield
---
UploadField:
defaultConfig:
autoUpload: true
allowedMaxFileNumber:
canUpload: true
canAttachExisting: 'CMS_ACCESS_AssetAdmin'
canPreviewFolder: true
previewMaxWidth: 80
previewMaxHeight: 60
uploadTemplateName: 'ss-uploadfield-uploadtemplate'
downloadTemplateName: 'ss-uploadfield-downloadtemplate'
overwriteWarning: true # Warning before overwriting existing file (only relevant when Upload: replaceFile is true)

View File

@ -53,7 +53,8 @@ doesn't necessarily have any visible styling.
## Files
* `[api:FileField]`: Simple file upload dialog.
* `[api:UploadField]`: File uploads through HTML5 features, including upload progress, preview and relationship management.
* `[api:UploadField]`: Upload to a `[api:File]` record, including upload progress, preview and relationship management.
* `[api:AssetField]`: Upload to a `[api:DBFile]` database field. Very similar to UploadField
## Relations

View File

@ -7,6 +7,10 @@ But that's not all it does - it will also link the uploaded file(s) to an existi
and let you edit the linked files as well. That makes it flexible enough to sometimes even
replace the GridField, like for instance in creating and managing a simple gallery.
The field automatically creates a `File` record for each uploaded file.
In order to associate uploaded files directly to a `DataObject` via the
`[api:DBFile]` database field, please use [AssetField](AssetField).
## Usage
The field can be used in three ways: To upload a single file into a `has_one` relationship,

View File

@ -0,0 +1,225 @@
# AssetField
## Introduction
This form field can be used to upload files into SilverStripe's asset store.
It associates a file directly to a `DataObject` through the `[api:DBFile]` database field.
Saving the file association directly in a `DataObject` (as opposed to a relation)
can simplify data management and publication.
In order to create `[api:File]` records to contain uploaded files,
please use the [AssetField](AssetField) instead.
## Usage
The field expects to save into a `DataObject` record with a `DBFile`
property matching the name of the field itself.
```php
class Team extends DataObject {
private static $db = array(
'BannerImage' => 'DBFile'
);
function getCMSFields() {
$fields = parent::getCMSFields();
$fields->addFieldToTab(
'Root.Upload',
$assetField = new AssetField(
$name = 'BannerImage',
$title = 'Upload a banner'
)
);
// Restrict validator to include only supported image formats
$assetField->setAllowedFileCategories('image/supported');
return $fields;
}
}
```
## Validation
Although images are uploaded and stored on the filesystem immediately after selection,
the value (or values) of this field will not be written to any related record until the
record is saved and successfully validated. However, any invalid records will still
persist across form submissions until explicitly removed or replaced by the user.
Care should be taken as invalid files may remain within the filesystem until explicitly removed.
## Configuration
### Overview
AssetField can either be configured on an instance level with the various getProperty
and setProperty functions, or globally by overriding the YAML defaults.
See the [Configuration Reference](uploadfield#configuration-reference) section for possible values.
Example: mysite/_config/uploadfield.yml
:::yaml
after: framework#uploadfield
---
AssetField:
defaultConfig:
canUpload: false
### Set a custom folder
This example will save all uploads in the `customfolder` in the configured assets store root (normally under 'assets')
If the folder doesn't exist, it will be created.
:::php
$fields->addFieldToTab(
'Root.Upload',
$assetField = new AssetField(
$name = 'GalleryImage',
$title = 'Please upload an image'
)
);
$assetField->setFolderName('customfolder');
### Limit the allowed filetypes
`AllowedExtensions` defaults to the `File.allowed_extensions` configuration setting,
but can be overwritten for each AssetField:
:::php
$assetField->setAllowedExtensions(array('jpg', 'jpeg', 'png', 'gif'));
Entire groups of file extensions can be specified in order to quickly limit types to known file categories.
This can be done by using file category names, which are defined via the `File.app_categories` config. This
list could be extended with any custom categories.
The built in categories are:
| File category | Example extensions |
|-----------------|--------------------|
| archive | zip, gz, rar |
| audio | mp3, wav, ogg |
| document | doc, txt, pdf |
| flash | fla, swf |
| image | jpg, tiff, ps |
| image/supported | jpg, gif, png |
| video | mkv, avi, mp4 |
Note that although all image types are included in the 'image' category, only images that are in the
'images/supported' list are compatible with the SilverStripe image manipulations API. Other types
can be uploaded, but cannot be resized.
:::php
$assetField->setAllowedFileCategories('image/supported');
This will limit files to the the compatible image formats: jpg, jpeg, gif, and png.
`AllowedExtensions` can also be set globally via the
[YAML configuration](/developer_guides/configuration/configuration/#configuration-yaml-syntax-and-rules),
for example you may add the following into your mysite/_config/config.yml:
:::yaml
File:
allowed_extensions:
- 7zip
- xzip
### Limit the maximum file size
`AllowedMaxFileSize` is by default set to the lower value of the 2 php.ini configurations:
`upload_max_filesize` and `post_max_size`. The value is set as bytes.
NOTE: this only sets the configuration for your AssetField, this does NOT change your
server upload settings, so if your server is set to only allow 1 MB and you set the
AssetField to 2 MB, uploads will not work.
:::php
$sizeMB = 2; // 2 MB
$size = $sizeMB * 1024 * 1024; // 2 MB in bytes
$this->getValidator()->setAllowedMaxFileSize($size);
You can also specify a default global maximum file size setting in your config for different file types.
This is overridden when specifying the max allowed file size on the AssetField instance.
:::yaml
Upload_Validator:
default_max_file_size:
'[image]': '1m'
'[document]': '5m'
'jpeg': 2000
### Preview dimensions
Set the dimensions of the image preview. By default the max width is set to 80 and the max height is set to 60.
:::php
$assetField->setPreviewMaxWidth(100);
$assetField->setPreviewMaxHeight(100);
### Disable uploading of new files
Alternatively, you can force the user to only specify already existing files in the file library
:::php
$assetField->setCanUpload(false);
### Automatic or manual upload
By default, the AssetField will try to automatically upload all selected files. Setting the `autoUpload`
property to false, will present you with a list of selected files that you can then upload manually one by one:
:::php
$assetField->setAutoUpload(false);
### Change Detection
The CMS interface will automatically notify the form containing
an AssetField instance of changes, such as a new upload,
or the removal of an existing upload (through a `dirty` event).
The UI can then choose an appropriate response (e.g. highlighting the "save" button).
If the AssetField doesn't save into a relation, there's technically no saveable change
(the upload has already happened), which is why this feature can be disabled on demand.
:::php
$assetField->setConfig('changeDetection', false);
## Configuration Reference
* `setAllowedFileExtensions`: (array) List of file extensions allowed.
* `setAllowedFileCategories`: (array|string) List of types of files allowed. May be any number of
categories as defined in `File.app_categories` config.
* `setAutoUpload`: (boolean) Should the field automatically trigger an upload once a file is selected?
* `setCanPreviewFolder`: (boolean|string) Can the user preview the folder files will be saved into?
String values are interpreted as permission codes.
* `setCanUpload`: (boolean|string) Can the user upload new files, or just select from existing files.
String values are interpreted as permission codes.
* `setDownloadTemplateName`: (string) javascript template used to display already uploaded files, see
javascript/UploadField_downloadtemplate.js.
* `setPreviewMaxWidth`: (int).
* `setPreviewMaxHeight`: (int).
* `setTemplateFileButtons`: (string) Template name to use for the file buttons.
* `setUploadTemplateName`: (string) javascript template used to display uploading files, see
javascript/UploadField_uploadtemplate.js.
* `setCanPreviewFolder`: (boolean|string) Is the upload folder visible to uploading users? String values
are interpreted as permission codes.

View File

@ -153,4 +153,10 @@ You may also notice the 'Sync files' button (highlighted below). This button all
## Upload
Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class.
Files can be managed through forms in three ways:
* `[api:FileField]`: provides a simple HTML input with a type of "file".
* [UploadField](/developer_guides/forms/field_types/uploadfield): more feature-rich field (
including AJAX-based uploads, previews, relationship management and file data management).
* [AssetField](/developer_guides/forms/field_types/assetfield): Similar to UploadField,
but operates on a `[api:DBFile]` database field instead of a `[api:File]` record.

View File

@ -20,6 +20,7 @@
the `DataObject::ID` in a `data-fileid` property, or via shortcodes. This is necessary because file
urls are no longer able to identify assets.
* Extension point `HtmlEditorField::processImage` has been removed, and moved to `Image::regenerateImageHTML`
* `Upload::load` now stores assets directly without saving into a `File` dataobject.
## New API
@ -31,6 +32,7 @@
cache or combined files).
* `Requirements_Minifier` API can be used to declare any new mechanism for minifying combined required files.
By default this api is provided by the `JSMinifier` class, but user code can substitute their own.
* `AssetField` formfield to provide an `UploadField` style uploader for the new `DBFile` database field.
## Deprecated classes/methods

View File

@ -124,16 +124,92 @@ class Upload extends Controller {
return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename));
}
/**
*
* @return AssetStore
*/
protected function getAssetStore() {
return Injector::inst()->get('AssetStore');
}
/**
* Save an file passed from a form post into the AssetStore directly
*
* @param $tmpFile array Indexed array that PHP generated for every file it uploads.
* @param $folderPath string Folder path relative to /assets
* @return array|false Either the tuple array, or false if the file could not be saved
*/
public function load($tmpFile, $folderPath = false) {
// Validate filename
$filename = $this->getValidFilename($tmpFile, $folderPath);
if(!$filename) {
return false;
}
// Save file into backend
$result = $this->storeTempFile($tmpFile, $filename, $this->getAssetStore());
//to allow extensions to e.g. create a version after an upload
$this->extend('onAfterLoad', $result);
return $result;
}
/**
* Save an file passed from a form post into this object.
* File names are filtered through {@link FileNameFilter}, see class documentation
* on how to influence this behaviour.
*
* @param $tmpFile array Indexed array that PHP generated for every file it uploads.
* @param $folderPath string Folder path relative to /assets
* @return Boolean|string Either success or error-message.
* @param array $tmpFile
* @param AssetContainer $file
* @return bool True if the file was successfully saved into this record
*/
public function load($tmpFile, $folderPath = false) {
public function loadIntoFile($tmpFile, $file = null, $folderPath = false) {
$this->file = $file;
// Validate filename
$filename = $this->getValidFilename($tmpFile, $folderPath);
if(!$filename) {
return false;
}
$filename = $this->resolveExistingFile($filename);
// Save changes to underlying record (if it's a DataObject)
$this->storeTempFile($tmpFile, $filename, $this->file);
if($this->file instanceof DataObject) {
$this->file->write();
}
//to allow extensions to e.g. create a version after an upload
$this->file->extend('onAfterUpload');
$this->extend('onAfterLoadIntoFile', $this->file);
return true;
}
/**
* Assign this temporary file into the given destination
*
* @param array $tmpFile
* @param string $filename
* @param AssetContainer|AssetStore $container
* @return array
*/
protected function storeTempFile($tmpFile, $filename, $container) {
// Save file into backend
$conflictResolution = $this->replaceFile
? AssetStore::CONFLICT_OVERWRITE
: AssetStore::CONFLICT_RENAME;
return $container->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $conflictResolution);
}
/**
* Given a temporary file and upload path, validate the file and determine the
* value of the 'Filename' tuple that should be used to store this asset.
*
* @param array $tmpFile
* @param string $folderPath
* @return string|false Value of filename tuple, or false if invalid
*/
protected function getValidFilename($tmpFile, $folderPath = false) {
if(!is_array($tmpFile)) {
throw new InvalidArgumentException(
"Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype"
@ -157,23 +233,7 @@ class Upload extends Controller {
if($folderPath) {
$filename = File::join_paths($folderPath, $filename);
}
// Validate filename
$filename = $this->resolveExistingFile($filename);
// Save file into backend
$conflictResolution = $this->replaceFile ? AssetStore::CONFLICT_OVERWRITE : AssetStore::CONFLICT_RENAME;
$this->file->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $conflictResolution);
// Save changes to underlying record (if it's a DataObject)
if($this->file instanceof DataObject) {
$this->file->write();
}
//to allow extensions to e.g. create a version after an upload
$this->file->extend('onAfterUpload');
$this->extend('onAfterLoad', $this->file);
return true;
return $filename;
}
/**
@ -226,18 +286,6 @@ class Upload extends Controller {
throw new Exception("Could not rename {$filename} with {$tries} tries");
}
/**
* Load temporary PHP-upload into File-object.
*
* @param array $tmpFile
* @param AssetContainer $file
* @return Boolean
*/
public function loadIntoFile($tmpFile, $file, $folderPath = false) {
$this->file = $file;
return $this->load($tmpFile, $folderPath);
}
/**
* @return Boolean
*/

View File

@ -87,13 +87,12 @@ class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandle
'Title' => 'Varchar',
'MimeType' => 'Varchar',
'String' => 'Text',
'Tag' => 'HTMLText'
'Tag' => 'HTMLText',
'Size' => 'Varchar'
);
public function scaffoldFormField($title = null, $params = null) {
return null;
// @todo
//return new AssetUploadField($this->getName(), $title);
return AssetField::create($this->getName(), $title);
}
/**
@ -241,13 +240,11 @@ class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandle
/**
* Get URL, but without resampling.
* Note that this will return the url even if the file does not exist.
*
* @return string
*/
public function getSourceURL() {
if(!$this->exists()) {
return null;
}
return $this
->getStore()
->getAsURL($this->Filename, $this->Hash, $this->Variant);
@ -294,7 +291,12 @@ class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandle
}
public function exists() {
return !empty($this->Filename);
if(empty($this->Filename)) {
return false;
}
return $this
->getStore()
->exists($this->Filename, $this->Hash, $this->Variant);
}
public static function get_shortcodes() {
@ -454,4 +456,18 @@ class DBFile extends CompositeDBField implements AssetContainer, ShortcodeHandle
return parent::setField($field, $value, $markChanged);
}
/**
* Returns the size of the file type in an appropriate format.
*
* @return string|false String value, or false if doesn't exist
*/
public function getSize() {
$size = $this->getAbsoluteSize();
if($size) {
return \File::format_size($size);
}
return false;
}
}

766
forms/AssetField.php Normal file
View File

@ -0,0 +1,766 @@
<?php
use SilverStripe\Filesystem\Storage\AssetContainer;
use SilverStripe\Filesystem\Storage\AssetStore;
/**
* Field for uploading into a DBFile instance.
*
* This formfield has fewer options than UploadField:
* - Assets can only be uploaded, not attached from library
* - Duplicate files will only be renamed, not allowed to overwrite existing references.
* - Only one file may be attached.
* - Files can't be edited once uploaded.
* - Attached files can only be removed, not deleted.
*
* @package forms
*/
class AssetField extends FileField {
/**
* @var array
*/
private static $allowed_actions = array(
'upload'
);
/**
* @var array
*/
private static $url_handlers = array(
'$Action!' => '$Action',
);
private static $casting = array(
'Value' => 'DBFile',
'UploadFieldThumbnailURL' => 'Varchar'
);
/**
* Template to use for the file button widget
*
* @var string
*/
protected $templateFileButtons = 'AssetField_FileButtons';
/**
* Parent data record. Will be infered from parent form or controller if blank. The destination
* DBFile should be a property of the name $name on this object.
*
* @var DataObject
*/
protected $record;
/**
* Config for this field used in the front-end javascript
* (will be merged into the config of the javascript file upload plugin).
*
* @var array
*/
protected $ufConfig = array();
/**
* Front end config defaults
*
* @config
* @var array
*/
private static $defaultConfig = array(
/**
* Automatically upload the file once selected
*
* @var boolean
*/
'autoUpload' => true,
/**
* Can the user upload new files.
* String values are interpreted as permission codes.
*
* @var boolean|string
*/
'canUpload' => true,
/**
* Shows the target folder for new uploads in the field UI.
* Disable to keep the internal filesystem structure hidden from users.
*
* @var boolean|string
*/
'canPreviewFolder' => true,
/**
* Indicate a change event to the containing form if an upload
* or file edit/delete was performed.
*
* @var boolean
*/
'changeDetection' => true,
/**
* Maximum width of the preview thumbnail
*
* @var integer
*/
'previewMaxWidth' => 80,
/**
* Maximum height of the preview thumbnail
*
* @var integer
*/
'previewMaxHeight' => 60,
/**
* javascript template used to display uploading files
*
* @see javascript/UploadField_uploadtemplate.js
* @var string
*/
'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
/**
* javascript template used to display already uploaded files
*
* @see javascript/UploadField_downloadtemplate.js
* @var string
*/
'downloadTemplateName' => 'ss-uploadfield-downloadtemplate'
);
/**
* Folder to display in "Select files" list.
* Defaults to listing all files regardless of folder.
* The folder path should be relative to the webroot.
* See {@link FileField->folderName} to set the upload target instead.
*
* @var string
* @example admin/folder/subfolder
*/
protected $displayFolderName;
/**
* Construct a new UploadField instance
*
* @param string $name The internal field name, passed to forms.
* @param string $title The field label.
* @param Form $form Reference to the container form
*/
public function __construct($name, $title = null) {
$this->addExtraClass('ss-upload'); // class, used by js
$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
$this->ufConfig = array_merge($this->ufConfig, self::config()->defaultConfig);
parent::__construct($name, $title);
// AssetField always uses rename replacement method
$this->getUpload()->setReplaceFile(false);
// filter out '' since this would be a regex problem on JS end
$this->getValidator()->setAllowedExtensions(
array_filter(Config::inst()->get('File', 'allowed_extensions'))
);
// get the lower max size
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
$maxPost = File::ini2bytes(ini_get('post_max_size'));
$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
}
/**
* Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
*
* @param string
* @return $this
*/
public function setTemplateFileButtons($template) {
$this->templateFileButtons = $template;
return $this;
}
/**
* @return string
*/
public function getTemplateFileButtons() {
return $this->templateFileButtons;
}
/**
* Determine if the target folder for new uploads in is visible the field UI.
*
* @return boolean
*/
public function canPreviewFolder() {
if(!$this->isActive()) {
return false;
}
$can = $this->getConfig('canPreviewFolder');
if(is_bool($can)) {
return $can;
}
return Permission::check($can);
}
/**
* Determine if the target folder for new uploads in is visible the field UI.
* Disable to keep the internal filesystem structure hidden from users.
*
* @param boolean|string $canPreviewFolder Either a boolean flag, or a
* required permission code
* @return $this Self reference
*/
public function setCanPreviewFolder($canPreviewFolder) {
return $this->setConfig('canPreviewFolder', $canPreviewFolder);
}
/**
* @param string
* @return $this
*/
public function setDisplayFolderName($name) {
$this->displayFolderName = $name;
return $this;
}
/**
* @return string
*/
public function getDisplayFolderName() {
return $this->displayFolderName;
}
/**
* Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
* @param DataObject $record
*/
public function setRecord($record) {
$this->record = $record;
return $this;
}
/**
* Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will
* use Form->getRecord().
*
* @return DataObject
*/
public function getRecord() {
if (!$this->record
&& $this->form
&& ($record = $this->form->getRecord())
&& $record instanceof DataObject
) {
$this->record = $record;
}
return $this->record;
}
public function setValue($value, $record = null) {
// Extract value from underlying record
if(empty($value) && $this->getName() && $record instanceof DataObject) {
$name = $this->getName();
$value = $record->$name;
}
// Convert asset container to tuple value
if($value instanceof AssetContainer) {
if($value->exists()) {
$value = array(
'Filename' => $value->getFilename(),
'Hash' => $value->getHash(),
'Variant' => $value->getVariant()
);
} else {
$value = null;
}
}
// If javascript is disabled, direct file upload (non-html5 style) can
// trigger a single or multiple file submission. Note that this may be
// included in addition to re-submitted File IDs as above, so these
// should be added to the list instead of operated on independently.
if($uploadedFile = $this->extractUploadedFileData($value)) {
$value = $this->saveTemporaryFile($uploadedFile, $error);
if(!$value) {
throw new ValidationException($error);
}
}
// Set value using parent
return parent::setValue($value, $record);
}
public function Value() {
// Re-override FileField Value to use data value
return $this->dataValue();
}
public function saveInto(DataObjectInterface $record) {
// Check required relation details are available
$name = $this->getName();
if(!$name) {
return $this;
}
$value = $this->Value();
foreach(array('Filename', 'Hash', 'Variant') as $part) {
$partValue = isset($value[$part])
? $value[$part]
: null;
$record->setField("{$name}{$part}", $partValue);
}
return $this;
}
/**
* Assign a front-end config variable for the upload field
*
* @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
*
* @param string $key
* @param mixed $val
* @return $this self reference
*/
public function setConfig($key, $val) {
$this->ufConfig[$key] = $val;
return $this;
}
/**
* Gets a front-end config variable for the upload field
*
* @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
*
* @param string $key
* @return mixed
*/
public function getConfig($key) {
if(isset($this->ufConfig[$key])) {
return $this->ufConfig[$key];
}
}
/**
* Determine if the field should automatically upload the file.
*
* @return boolean
*/
public function getAutoUpload() {
return $this->getConfig('autoUpload');
}
/**
* Determine if the field should automatically upload the file
*
* @param boolean $autoUpload
* @return $this Self reference
*/
public function setAutoUpload($autoUpload) {
return $this->setConfig('autoUpload', $autoUpload);
}
/**
* Determine if the user has permission to upload.
*
* @return boolean
*/
public function canUpload() {
if(!$this->isActive()) {
return false;
}
$can = $this->getConfig('canUpload');
if(is_bool($can)) {
return $can;
}
return Permission::check($can);
}
/**
* Specify whether the user can upload files.
* String values will be treated as required permission codes
*
* @param bool|string $canUpload Either a boolean flag, or a required
* permission code
* @return $this Self reference
*/
public function setCanUpload($canUpload) {
return $this->setConfig('canUpload', $canUpload);
}
/**
* Returns true if the field is neither readonly nor disabled
*
* @return bool
*/
public function isActive() {
return !$this->isDisabled() && !$this->isReadonly();
}
/**
* Gets thumbnail width. Defaults to 80
*
* @return int
*/
public function getPreviewMaxWidth() {
return $this->getConfig('previewMaxWidth');
}
/**
* Set thumbnail width.
*
* @param int $previewMaxWidth
* @return $this Self reference
*/
public function setPreviewMaxWidth($previewMaxWidth) {
return $this->setConfig('previewMaxWidth', $previewMaxWidth);
}
/**
* Gets thumbnail height. Defaults to 60
*
* @return int
*/
public function getPreviewMaxHeight() {
return $this->getConfig('previewMaxHeight');
}
/**
* Set thumbnail height.
*
* @param int $previewMaxHeight
* @return $this Self reference
*/
public function setPreviewMaxHeight($previewMaxHeight) {
return $this->setConfig('previewMaxHeight', $previewMaxHeight);
}
/**
* javascript template used to display uploading files
* Defaults to 'ss-uploadfield-uploadtemplate'
*
* @see javascript/UploadField_uploadtemplate.js
* @var string
*/
public function getUploadTemplateName() {
return $this->getConfig('uploadTemplateName');
}
/**
* Set javascript template used to display uploading files
*
* @param string $uploadTemplateName
* @return $this Self reference
*/
public function setUploadTemplateName($uploadTemplateName) {
return $this->setConfig('uploadTemplateName', $uploadTemplateName);
}
/**
* javascript template used to display already uploaded files
* Defaults to 'ss-downloadfield-downloadtemplate'
*
* @see javascript/DownloadField_downloadtemplate.js
* @var string
*/
public function getDownloadTemplateName() {
return $this->getConfig('downloadTemplateName');
}
/**
* Set javascript template used to display already uploaded files
*
* @param string $downloadTemplateName
* @return $this Self reference
*/
public function setDownloadTemplateName($downloadTemplateName) {
return $this->setConfig('downloadTemplateName', $downloadTemplateName);
}
public function Field($properties = array()) {
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js');
Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js');
Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang');
Requirements::combine_files('uploadfield.js', array(
// @todo jquery templates is a project no longer maintained and should be retired at some point.
THIRDPARTY_DIR . '/javascript-templates/tmpl.js',
THIRDPARTY_DIR . '/javascript-loadimage/load-image.js',
THIRDPARTY_DIR . '/jquery-fileupload/jquery.iframe-transport.js',
THIRDPARTY_DIR . '/jquery-fileupload/cors/jquery.xdr-transport.js',
THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload.js',
THIRDPARTY_DIR . '/jquery-fileupload/jquery.fileupload-ui.js',
FRAMEWORK_DIR . '/javascript/UploadField_uploadtemplate.js',
FRAMEWORK_DIR . '/javascript/UploadField_downloadtemplate.js',
FRAMEWORK_DIR . '/javascript/UploadField.js',
));
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); // TODO hmmm, remove it?
Requirements::css(FRAMEWORK_DIR . '/css/UploadField.css');
// Calculated config as per jquery.fileupload-ui.js
$config = array(
'allowedMaxFileNumber' => 1, // Only one file allowed for AssetField
'url' => $this->Link('upload'),
'urlSelectDialog' => $this->Link('select'),
'urlAttach' => $this->Link('attach'),
'urlFileExists' => $this->link('fileexists'),
'acceptFileTypes' => '.+$',
// Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
'maxNumberOfFiles' => $this->Value() ? 0 : 1,
'replaceFile' => false, // Should always be false for AssetField
);
// Validation: File extensions
if ($allowedExtensions = $this->getAllowedExtensions()) {
$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
$config['errorMessages']['acceptFileTypes'] = _t(
'File.INVALIDEXTENSIONSHORT',
'Extension is not allowed'
);
}
// Validation: File size
if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
$config['maxFileSize'] = $allowedMaxFileSize;
$config['errorMessages']['maxFileSize'] = _t(
'File.TOOLARGESHORT',
'Filesize exceeds {size}',
array('size' => File::format_size($config['maxFileSize']))
);
}
$mergedConfig = array_merge($config, $this->ufConfig);
return $this->customise(array(
'ConfigString' => Convert::raw2json($mergedConfig),
'UploadFieldFileButtons' => $this->renderWith($this->getTemplateFileButtons())
))->renderWith($this->getTemplates());
}
/**
* Validation method for this field, called when the entire form is validated
*
* @param Validator $validator
* @return boolean
*/
public function validate($validator) {
$name = $this->getName();
$value = $this->Value();
// If there is no file then quit
if(!$value) {
return true;
}
// Revalidate each file against nested validator
$this->getUpload()->clearErrors();
// Generate $_FILES style file attribute array for upload validator
$store = $this->getAssetStore();
$mime = $store->getMimeType($value['Filename'], $value['Hash'], $value['Variant']);
$metadata = $store->getMetadata($value['Filename'], $value['Hash'], $value['Variant']);
$tmpFile = array(
'name' => $value['Filename'],
'type' => $mime,
'size' => isset($metadata['size']) ? $metadata['size'] : 0,
'tmp_name' => null, // Should bypass is_uploaded_file check
'error' => UPLOAD_ERR_OK,
);
$this->getUpload()->validate($tmpFile);
// Check all errors
if($errors = $this->getUpload()->getErrors()) {
foreach($errors as $error) {
$validator->validationError($name, $error, "validation");
}
return false;
}
return true;
}
/**
* Given an array of post variables, extract all temporary file data into an array
*
* @param array $postVars Array of posted form data
* @return array data for uploaded file
*/
protected function extractUploadedFileData($postVars) {
// Note: Format of posted file parameters in php is a feature of using
// <input name='{$Name}[Upload]' /> for multiple file uploads
// Skip empty file
if(empty($postVars['tmp_name'])) {
return null;
}
// Return single level array for posted file
if(empty($postVars['tmp_name']['Upload'])) {
return $postVars;
}
// Extract posted feedback value
$tmpFile = array();
foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) {
$tmpFile[$field] = $postVars[$field]['Upload'];
}
return $tmpFile;
}
/**
* Loads the temporary file data into the asset store, and return the tuple details
* for the result.
*
* @param array $tmpFile Temporary file data
* @param string $error Error message
* @return array Result of saved file, or null if error
*/
protected function saveTemporaryFile($tmpFile, &$error = null) {
$error = null;
if (empty($tmpFile)) {
$error = _t('UploadField.FIELDNOTSET', 'File information not found');
return null;
}
if($tmpFile['error']) {
$error = $tmpFile['error'];
return null;
}
// Get the uploaded file into a new file object.
try {
$result = $this
->getUpload()
->load($tmpFile, $this->getFolderName());
} catch (Exception $e) {
// we shouldn't get an error here, but just in case
$error = $e->getMessage();
return null;
}
// Check if upload field has an error
if ($this->getUpload()->isError()) {
$error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors());
return null;
}
// return tuple array of Filename, Hash and Variant
return $result;
}
/**
* Safely encodes the File object with all standard fields required
* by the front end
*
* @param string $filename
* @param string $hash
* @param string $variant
* @return array Encoded list of file attributes
*/
protected function encodeAssetAttributes($filename, $hash, $variant) {
// Force regeneration of file thumbnail for this tuple (without saving into db)
$object = DBFile::create();
$object->setValue(array('Filename' => $filename, 'Hash' => $hash, 'Variant' => $variant));
return array(
'filename' => $filename,
'hash' => $hash,
'variant' => $variant,
'name' => $object->getBasename(),
'url' => $object->getURL(),
'thumbnail_url' => $object->ThumbnailURL(
$this->getPreviewMaxWidth(),
$this->getPreviewMaxHeight()
),
'size' => $object->getAbsoluteSize(),
'type' => File::get_file_type($object->getFilename()),
'buttons' => (string)$this->renderWith($this->getTemplateFileButtons()),
'fieldname' => $this->getName()
);
}
/**
* Action to handle upload of a single file
*
* @param SS_HTTPRequest $request
* @return SS_HTTPResponse
*/
public function upload(SS_HTTPRequest $request) {
if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
return $this->httpError(403);
}
// Protect against CSRF on destructive action
$token = $this
->getForm()
->getSecurityToken();
if(!$token->checkRequest($request)) {
return $this->httpError(400);
}
// Get form details
$name = $this->getName();
$postVars = $request->postVar($name);
// Extract uploaded files from Form data
$uploadedFile = $this->extractUploadedFileData($postVars);
if(!$uploadedFile) {
return $this->httpError(400);
}
// Save the temporary files into a File objects
// and save data/error on a per file basis
$result = $this->saveTemporaryFile($uploadedFile, $error);
if(empty($result)) {
$return = array('error' => $error);
} else {
$return = $this->encodeAssetAttributes($result['Filename'], $result['Hash'], $result['Variant']);
}
$this
->getUpload()
->clearErrors();
// Format response with json
$response = new SS_HTTPResponse(Convert::raw2json(array($return)));
$response->addHeader('Content-Type', 'text/plain');
return $response;
}
public function performReadonlyTransformation() {
$clone = clone $this;
$clone->addExtraClass('readonly');
$clone->setReadonly(true);
return $clone;
}
/**
* Gets the foreign class that needs to be created, or 'File' as default if there
* is no relationship, or it cannot be determined.
*
* @param $default Default value to return if no value could be calculated
* @return string Foreign class name.
*/
public function getRelationAutosetClass($default = 'File') {
// Don't autodetermine relation if no relationship between parent record
if(!$this->relationAutoSetting) return $default;
// Check record and name
$name = $this->getName();
$record = $this->getRecord();
if(empty($name) || empty($record)) {
return $default;
} else {
$class = $record->getRelationClass($name);
return empty($class) ? $default : $class;
}
}
/**
* @return AssetStore
*/
protected function getAssetStore() {
return Injector::inst()->get('AssetStore');
}
}

View File

@ -460,7 +460,15 @@ class FormField extends RequestHandler {
//
// CSS class needs to be different from the one rendered through {@link FieldHolder()}.
if($this->Message()) {
$classes[] .= 'holder-' . $this->MessageType();
$classes[] = 'holder-' . $this->MessageType();
}
if($this->isDisabled()) {
$classes[] = 'disabled';
}
if($this->isReadonly()) {
$classes[] = 'readonly';
}
return implode(' ', $classes);

View File

@ -80,11 +80,18 @@ class UploadField extends FileField {
/**
* Config for this field used in the front-end javascript
* (will be merged into the config of the javascript file upload plugin).
* See framework/_config/uploadfield.yml for configuration defaults and documentation.
*
* @var array
*/
protected $ufConfig = array(
protected $ufConfig = array();
/**
* Front end config defaults
*
* @config
* @var array
*/
private static $defaultConfig = array(
/**
* Automatically upload the file once selected
*
@ -212,7 +219,7 @@ class UploadField extends FileField {
$this->addExtraClass('ss-upload'); // class, used by js
$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
$this->ufConfig = array_merge($this->ufConfig, self::config()->defaultConfig);
$this->ufConfig = self::config()->defaultConfig;
parent::__construct($name, $title);
@ -905,17 +912,6 @@ class UploadField extends FileField {
);
}
public function extraClass() {
if($this->isDisabled()) {
$this->addExtraClass('disabled');
}
if($this->isReadonly()) {
$this->addExtraClass('readonly');
}
return parent::extraClass();
}
public function Field($properties = array()) {
Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js');
Requirements::javascript(THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js');

View File

@ -7,9 +7,14 @@ window.tmpl.cache['ss-uploadfield-downloadtemplate'] = tmpl(
'</span></div>' +
'{% } %}' +
'<div class="ss-uploadfield-item-info">' +
'{% if (!file.error) { %}' +
'{% if (!file.error && file.id) { %}' +
'<input type="hidden" name="{%=file.fieldname%}[Files][]" value="{%=file.id%}" />' +
'{% } %}' +
'{% if (!file.error && file.filename) { %}' +
'<input type="hidden" value="{%=file.filename%}" name="{%=file.fieldname%}[Filename]" />' +
'<input type="hidden" value="{%=file.hash%}" name="{%=file.fieldname%}[Hash]" />' +
'<input type="hidden" value="{%=file.variant%}" name="{%=file.fieldname%}[Variant]" />' +
'{% } %}' +
'<label class="ss-uploadfield-item-name">' +
'<span class="name" title="{%=file.name%}">{%=file.name%}</span> ' +
'<span class="size">{%=o.formatFileSize(file.size)%}</span>' +

View File

@ -23,7 +23,7 @@ window.tmpl.cache['ss-uploadfield-uploadtemplate'] = tmpl(
'<div class="ss-uploadfield-item-cancel cancel">' +
'<button class="icon icon-16" data-icon="minus-circle" title="' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '">' + ss.i18n._t('UploadField.CANCELREMOVE', 'Cancel/Remove') + '</button>' +
'</div>' +
'<div class="ss-uploadfield-item-overwrite hide ">'+
'<div class="ss-uploadfield-item-overwrite hide ">' +
'<button data-icon="drive-upload" class="ss-uploadfield-item-overwrite-warning" title="' + ss.i18n._t('UploadField.OVERWRITE', 'Overwrite') + '">' + ss.i18n._t('UploadField.OVERWRITE', 'Overwrite') + '</button>' +
'</div>' +
'</div>' +

View File

@ -128,8 +128,10 @@ abstract class CompositeDBField extends DBField {
* If $record is assigned to a dataobject, this field becomes a loose wrapper over
* the records on that object instead.
*
* {@see ViewableData::obj}
*
* @param mixed $value
* @param DataObject $record
* @param mixed $record Parent object to this field, which could be a DataObject, record array, or other
* @param bool $markChanged
*/
public function setValue($value, $record = null, $markChanged = true) {
@ -154,7 +156,7 @@ abstract class CompositeDBField extends DBField {
// Load from $record
$key = $this->getName() . $field;
if(isset($record[$key])) {
if(is_array($record) && isset($record[$key])) {
$this->setField($field, $record[$key]);
}
}

48
templates/AssetField.ss Normal file
View File

@ -0,0 +1,48 @@
<ul class="ss-uploadfield-files files">
<% if $Value %>
<li class="ss-uploadfield-item template-download" data-filename="$Value.Filename">
<div class="ss-uploadfield-item-preview preview"><span>
<img alt="$Name.ATT" src="$Value.ThumbnailURL($PreviewMaxWidth,$PreviewMaxHeight).ATT" />
</span></div>
<div class="ss-uploadfield-item-info">
<input type='hidden' value='$Value.Filename.ATT' name='{$Name}[Filename]' />
<input type='hidden' value='$Value.Hash.ATT' name='{$Name}[Hash]' />
<input type='hidden' value='$Value.Variant.ATT' name='{$Name}[Variant]' />
<label class="ss-uploadfield-item-name">
<span class="name">$Value.Basename.XML</span>
<span class="size">$Value.Size.XML</span>
<div class="clear"><!-- --></div>
</label>
<div class="ss-uploadfield-item-actions">
<% if $isActive %>
$UploadFieldFileButtons.RAW
<% end_if %>
</div>
</div>
</li>
<% end_if %>
</ul>
<% if $canUpload %>
<div class="ss-uploadfield-item ss-uploadfield-addfile<% if $CustomisedItems %> borderTop<% end_if %>">
<div class="ss-uploadfield-item-preview ss-uploadfield-dropzone ui-corner-all">
<%t UploadField.DROPFILE 'drop a file' %>
</div>
<div class="ss-uploadfield-item-info">
<label class="ss-uploadfield-item-name">
<b><%t UploadField.ATTACHFILE 'Attach a file' %></b>
<% if $canPreviewFolder %>
<small>(<%t UploadField.UPLOADSINTO 'saves into /{path}' path=$FolderName %>)</small>
<% end_if %>
</label>
<label class="ss-uploadfield-fromcomputer ss-ui-button ui-corner-all" title="<%t UploadField.FROMCOMPUTERINFO 'Upload from your computer' %>" data-icon="drive-upload">
<%t UploadField.FROMCOMPUTER 'From your computer' %>
<input id="$ID" name="{$Name}[Upload]" class="$extraClass ss-uploadfield-fromcomputer-fileinput" data-config="{$ConfigString.ATT}" type="file" />
</label>
<% if not $autoUpload %>
<button class="ss-uploadfield-startall ss-ui-button ui-corner-all" data-icon="navigation"><%t UploadField.STARTALL 'Start all' %></button>
<% end_if %>
<div class="clear"><!-- --></div>
</div>
<div class="clear"><!-- --></div>
</div>
<% end_if %>

View File

@ -0,0 +1,2 @@
<button class="ss-uploadfield-item-remove ss-ui-button ui-corner-all" title="<%t AssetUploadField.REMOVEINFO 'Remove this file from this field' %>" data-icon="plug-disconnect-prohibition">
<%t UploadField.REMOVE 'Remove' %></button>

View File

@ -41,7 +41,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u1 = new Upload();
$u1->setValidator($v);
$u1->load($tmpFile);
$u1->loadIntoFile($tmpFile);
$file1 = $u1->getFile();
$this->assertEquals(
'Uploads/UploadTest-testUpload.txt',
@ -59,7 +59,7 @@ class UploadTest extends SapphireTest {
// test upload into custom folder
$customFolder = 'UploadTest-testUpload';
$u2 = new Upload();
$u2->load($tmpFile, $customFolder);
$u2->loadIntoFile($tmpFile, null, $customFolder);
$file2 = $u2->getFile();
$this->assertEquals(
'UploadTest-testUpload/UploadTest-testUpload.txt',
@ -99,17 +99,17 @@ class UploadTest extends SapphireTest {
$v->setAllowedMaxFileSize(array('txt' => 10));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[document]' => 10));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('txt' => 200000));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
// check max file size set by app category
@ -128,17 +128,17 @@ class UploadTest extends SapphireTest {
$v->setAllowedMaxFileSize(array('[image]' => '40k'));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertTrue($result, 'Load failed with setting max file size');
$v->setAllowedMaxFileSize(array('[image]' => '1k'));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
$v->setAllowedMaxFileSize(array('[image]' => 1000));
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
}
@ -213,7 +213,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u1 = new Upload();
$u1->setValidator($v);
$result = $u1->load($tmpFile);
$result = $u1->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because size was too big');
}
@ -242,7 +242,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$result = $u->load($tmpFile);
$result = $u->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because extension was not accepted');
}
@ -271,7 +271,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertFileExists(
AssetStoreTest_SpyStore::getLocalPath($file),
@ -302,7 +302,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$result = $u->load($tmpFile);
$result = $u->loadIntoFile($tmpFile);
$this->assertFalse($result, 'Load failed because extension was not accepted');
$this->assertEquals(1, count($u->getErrors()), 'There is a single error of the file extension');
@ -328,7 +328,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload.tar.gz',
@ -341,7 +341,7 @@ class UploadTest extends SapphireTest {
);
$u = new Upload();
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2.tar.gz',
@ -359,7 +359,7 @@ class UploadTest extends SapphireTest {
);
$u = new Upload();
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file3 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v3.tar.gz',
@ -401,7 +401,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
@ -416,7 +416,7 @@ class UploadTest extends SapphireTest {
$u = new Upload();
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload-v2',
@ -458,7 +458,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
@ -474,7 +474,7 @@ class UploadTest extends SapphireTest {
$u = new Upload();
$u->setValidator($v);
$u->setReplaceFile(true);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file2 = $u->getFile();
$this->assertEquals(
'UploadTest-testUpload',
@ -516,7 +516,7 @@ class UploadTest extends SapphireTest {
// test upload into default folder
$u = new Upload();
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
$file = $u->getFile();
$this->assertEquals(
@ -595,7 +595,7 @@ class UploadTest extends SapphireTest {
$u = new Upload();
$u->setReplaceFile(true);
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
return $u->getFile();
};
@ -637,7 +637,7 @@ class UploadTest extends SapphireTest {
$u = new Upload();
$u->setReplaceFile(false);
$u->setValidator($v);
$u->load($tmpFile);
$u->loadIntoFile($tmpFile);
return $u->getFile();
};

View File

@ -28,7 +28,7 @@ class HtmlEditorFieldTest extends FunctionalTest {
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$fromPath = BASE_PATH . '/framework/tests/forms/images/' . $file->Name;
$destPath = BASE_PATH . $file->getURL(); // Only correct for test asset store
$destPath = AssetStoreTest_SpyStore::getLocalPath($file); // Only correct for test asset store
SS_Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);
}

View File

@ -0,0 +1,404 @@
<?php
/**
* @package framework
* @subpackage tests
*/
class AssetFieldTest extends FunctionalTest {
protected static $fixture_file = 'AssetFieldTest.yml';
protected $extraDataObjects = array(
'AssetFieldTest_Object'
);
public function setUp() {
parent::setUp();
// Set backend root to /AssetFieldTest
AssetStoreTest_SpyStore::activate('AssetFieldTest');
$create = function($path) {
Filesystem::makeFolder(dirname($path));
$fh = fopen($path, "w+");
fwrite($fh, str_repeat('x', 1000000));
fclose($fh);
};
// Write all DBFile references
foreach(AssetFieldTest_Object::get() as $object) {
$path = AssetStoreTest_SpyStore::getLocalPath($object->File);
$create($path);
}
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$path = AssetStoreTest_SpyStore::getLocalPath($file);
$create($path);
}
}
public function tearDown() {
AssetStoreTest_SpyStore::reset();
parent::tearDown();
}
/**
* 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);
$responseJSON = json_decode($response->getBody(), true);
$this->assertFalse($response->isError());
$this->assertEquals('MyDocuments/testUploadBasic.txt', $responseJSON[0]['filename']);
$this->assertEquals('315ae4c3d44412baa0c81515b6fb35829a337a5a', $responseJSON[0]['hash']);
$this->assertEmpty($responseJSON[0]['variant']);
$this->assertFileExists(
BASE_PATH . '/assets/AssetFieldTest/MyDocuments/315ae4c3d4/testUploadBasic.txt'
);
}
/**
* Test that an object can be uploaded against a DBFile field
*/
public function testUploadDBFile() {
$this->loginWithPermission('ADMIN');
// Unset existing has_one relation before re-uploading
$record = $this->objFromFixture('AssetFieldTest_Object', 'object1');
$record->FileFilename = null;
$record->FileHash = null;
$record->write();
// Firstly, ensure the file can be uploaded
$tmpFileName = 'testUploadHasOneRelation.txt';
$response = $this->mockFileUpload('File', $tmpFileName);
$responseJSON = json_decode($response->getBody(), true);
$this->assertFalse($response->isError());
$this->assertFileExists(
BASE_PATH . '/assets/AssetFieldTest/MyFiles/315ae4c3d4/testUploadHasOneRelation.txt'
);
// Secondly, ensure that simply uploading an object does not save the file against the relation
$record = AssetFieldTest_Object::get()->byID($record->ID);
$this->assertFalse($record->File->exists());
// Thirdly, test submitting the form with the encoded data
$response = $this->mockUploadFileSave(
'File',
$responseJSON[0]['filename'],
$responseJSON[0]['hash'],
$responseJSON[0]['variant']
);
$this->assertEmpty($response['errors']);
$record = AssetFieldTest_Object::get()->byID($record->ID);
$this->assertTrue($record->File->exists());
$this->assertEquals('315ae4c3d44412baa0c81515b6fb35829a337a5a', $record->File->Hash);
$this->assertEquals('MyFiles/testUploadHasOneRelation.txt', $record->File->Filename);
$this->assertEmpty($record->File->Variant);
}
/**
* Partially covered by {@link UploadTest->testUploadAcceptsAllowedExtension()},
* but this test additionally verifies that those constraints are actually enforced
* in this controller method.
*/
public function testAllowedExtensions() {
$this->loginWithPermission('ADMIN');
// Test invalid file
// Relies on Upload_Validator failing to allow this extension
$response = $this->mockFileUpload('File', 'invalid.php');
$response = json_decode($response->getBody(), true);
$this->assertTrue(array_key_exists('error', $response[0]));
$this->assertContains('Extension is not allowed', $response[0]['error']);
// Test valid file
$response = $this->mockFileUpload('File', 'valid.txt');
$response = json_decode($response->getBody(), true);
$this->assertFalse(array_key_exists('error', $response[0]));
// Test that allowed files cannot be uploaded to restricted field
$response = $this->mockFileUpload('Image', 'valid.txt');
$response = json_decode($response->getBody(), true);
$this->assertTrue(array_key_exists('error', $response[0]));
$this->assertContains('Extension is not allowed', $response[0]['error']);
}
/**
* Test that files can be removed from an existing field
*/
public function testRemoveFromHasOne() {
$record = $this->objFromFixture('AssetFieldTest_Object', 'object1');
// Check record exists
$this->assertTrue($record->File->exists());
$filePath = AssetStoreTest_SpyStore::getLocalPath($record->File);
$this->assertFileExists($filePath);
// Remove from record
$response = $this->mockUploadFileSave('File', null, null, null);
$this->assertEmpty($response['errors']);
// Check file is removed
$record = AssetFieldTest_Object::get()->byID($record->ID);
$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');
}
/**
* Test control output html
*/
public function testView() {
$this->loginWithPermission('ADMIN');
$record = $this->objFromFixture('AssetFieldTest_Object', 'object1');
// Requesting form is not an error
$response = $this->get('AssetFieldTest_Controller');
$this->assertFalse($response->isError());
// File exists in this response
$parser = new CSSContentParser($response->getBody());
$tuple = array();
$result = $parser->getBySelector(
"#AssetFieldTest_Form_Form_File_Holder .ss-uploadfield-files .ss-uploadfield-item input[type='hidden']"
);
foreach($result as $part) {
$name = (string)$part['name'];
$value = (string)$part['value'];
switch($name) {
case 'File[Filename]':
$tuple['Filename'] = $value;
break;
case 'File[Hash]':
$tuple['Hash'] = $value;
break;
case 'File[Variant]':
$tuple['Variant'] = $value;
break;
}
}
// Assert this value is correct
$expected = array(
'Filename' => 'MyFiles/subfolder1/file-subfolder.txt',
'Hash' => '55b443b60176235ef09801153cca4e6da7494a0c',
'Variant' => '',
);
$this->assertEquals($expected, $record->File->getValue());
$this->assertEquals($expected, $tuple);
}
public function testGetRecord() {
$record = $this->objFromFixture('AssetFieldTest_Object', 'object1');
$form = $this->getMockForm();
$field = AssetField::create('MyField');
$field->setForm($form);
$this->assertNull($field->getRecord(), 'Returns no record by default');
$field = AssetField::create('MyField');
$field->setForm($form);
$form->loadDataFrom($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record from form if available');
$field = AssetField::create('MyField');
$field->setForm($form);
$field->setRecord($record);
$this->assertEquals($record, $field->getRecord(), 'Returns record when set explicitly');
}
/**
* Test that getValue() / Value() methods work
*/
public function testValue() {
$record = $this->objFromFixture('AssetFieldTest_Object', 'object1');
// File field
$field = AssetField::create('File');
$this->assertEmpty($field->Value());
$field->setValue(null, $record);
$this->assertEquals(array(
'Filename' => 'MyFiles/subfolder1/file-subfolder.txt',
'Hash' => '55b443b60176235ef09801153cca4e6da7494a0c',
'Variant' => null,
), $field->Value());
// Empty field
$field = AssetField::create('Image');
$this->assertEmpty($field->Value());
$field->setValue(null, $record);
$this->assertEmpty($field->Value());
// Set via file (copies only tuple not the actual file reference)
$file = $this->objFromFixture('File', 'file1');
$field->setValue($file);
$this->assertEquals(array(
'Filename' => 'MyAssets/file1.txt',
'Hash' => '55b443b60176235ef09801153cca4e6da7494a0c',
'Variant' => null,
), $field->Value());
}
public function testCanUploadWithPermissionCode() {
$field = AssetField::create('MyField');
$field->setCanUpload(true);
$this->assertTrue($field->canUpload());
$field->setCanUpload(false);
$this->assertFalse($field->canUpload());
$field->setCanUpload('ADMIN');
$this->assertFalse($field->canUpload());
$this->loginWithPermission('ADMIN');
$field->setCanUpload(false);
$this->assertFalse($field->canUpload());
$field->setCanUpload('ADMIN');
$this->assertTrue($field->canUpload());
}
protected function getMockForm() {
return new Form(new Controller(), 'Form', new FieldList(), new FieldList());
}
/**
* @return Array Emulating an entry in the $_FILES superglobal
*/
protected function getUploadFile($tmpFileName = 'AssetFieldTest-testUpload.txt') {
$tmpFilePath = TEMP_FOLDER . '/' . $tmpFileName;
$tmpFileContent = '';
for($i=0; $i<10000; $i++) $tmpFileContent .= '0';
file_put_contents($tmpFilePath, $tmpFileContent);
// emulates the $_FILES array
// Notice that unlike UploadFieldTest::getUploadFile the key is 'Upload' not 'Uploads'
// and the value is a literal not an array
return array(
'name' => array('Upload' => $tmpFileName),
'type' => array('Upload' => 'text/plaintext'),
'size' => array('Upload' => filesize($tmpFilePath)),
'tmp_name' => array('Upload' => $tmpFilePath),
'error' => array('Upload' => UPLOAD_ERR_OK),
);
}
/**
* Simulates a form post to the test controller with the specified file tuple (Filename, Hash, Variant)
*
* @param string $fileField Name of field to assign ids to
* @param array $ids list of file IDs
* @return boolean Array with key 'errors'
*/
protected function mockUploadFileSave($fileField, $filename, $hash, $variant = null) {
// collate file ids
$data = array(
'action_submit' => 1,
$fileField => array(
'Filename' => $filename,
'Hash' => $hash,
'Variant' => $variant
)
);
$form = new AssetFieldTest_Form();
$form->loadDataFrom($data, true);
if($form->validate()) {
$record = $form->getRecord();
$form->saveInto($record);
$record->write();
return array('errors' => null);
} else {
return array('errors' => $form->getValidator()->getErrors());
}
}
/**
* Simulates a file upload
*
* @param string $fileField Name of the field to mock upload for
* @param array $tmpFileName Name of temporary file to upload
* @return SS_HTTPResponse form response
*/
protected function mockFileUpload($fileField, $tmpFileName) {
$upload = $this->getUploadFile($tmpFileName);
$_FILES = array($fileField => $upload);
return $this->post(
"AssetFieldTest_Controller/Form/field/{$fileField}/upload",
array($fileField => $upload)
);
}
}
class AssetFieldTest_Object extends DataObject implements TestOnly {
private static $db = array(
"Title" => "Text",
"File" => "DBFile",
"Image" => "DBFile('image/supported')"
);
}
class AssetFieldTest_Form extends Form implements TestOnly {
public function getRecord() {
if(empty($this->record)) {
$this->record = AssetFieldTest_Object::get()
->filter('Title', 'Object1')
->first();
}
return $this->record;
}
function __construct($controller = null, $name = 'Form') {
if(empty($controller)) {
$controller = new AssetFieldTest_Controller();
}
$fields = new FieldList(
AssetField::create('File')
->setFolderName('MyFiles'),
AssetField::create('Image')
->setAllowedFileCategories('image/supported')
->setFolderName('MyImages'),
AssetField::create('NoRelationField')
->setFolderName('MyDocuments')
);
$actions = new FieldList(
new FormAction('submit')
);
$validator = new RequiredFields();
parent::__construct($controller, $name, $fields, $actions, $validator);
$this->loadDataFrom($this->getRecord());
}
public function submit($data, Form $form) {
$record = $this->getRecord();
$form->saveInto($record);
$record->write();
return json_encode($record->toMap());
}
}
class AssetFieldTest_Controller extends Controller implements TestOnly {
protected $template = 'BlankPage';
private static $allowed_actions = array('Form');
public function Form() {
return new AssetFieldTest_Form($this, 'Form');
}
}

View File

@ -0,0 +1,17 @@
Folder:
folder1:
Name: MyAssets
File:
file1:
Title: File1
FileFilename: MyAssets/file1.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c
Name: file1.txt
Parent: =>Folder.folder1
AssetFieldTest_Object:
object1:
Title: 'Object1'
FileFilename: MyFiles/subfolder1/file-subfolder.txt
FileHash: 55b443b60176235ef09801153cca4e6da7494a0c

View File

@ -26,7 +26,7 @@ class DataDifferencerTest extends SapphireTest {
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $file) {
$fromPath = BASE_PATH . '/framework/tests/model/testimages/' . $file->Name;
$destPath = BASE_PATH . $file->getURL(); // Only correct for test asset store
$destPath = AssetStoreTest_SpyStore::getLocalPath($file); // Only correct for test asset store
SS_Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);
}

View File

@ -31,7 +31,7 @@ class ImageTest extends SapphireTest {
// Copy test images for each of the fixture references
$files = File::get()->exclude('ClassName', 'Folder');
foreach($files as $image) {
$filePath = BASE_PATH . $image->getURL(); // Only correct for test asset store
$filePath = AssetStoreTest_SpyStore::getLocalPath($image); // Only correct for test asset store
$sourcePath = BASE_PATH . '/framework/tests/model/testimages/' . $image->Name;
if(!file_exists($filePath)) {
SS_Filesystem::makeFolder(dirname($filePath));

View File

@ -1392,9 +1392,6 @@ class Requirements_Backend {
break;
}
}
if($combinedURL && !$included) {
throw new Exception("Failed to merge combined file {$combinedFile} with existing requirements");
}
}
}