PSR2 cleanup

This commit is contained in:
Damian Mooyman 2016-11-29 16:18:48 +13:00
parent d4abfea4eb
commit bc19b2a491
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
7 changed files with 2772 additions and 2625 deletions

View File

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

View File

@ -8,7 +8,7 @@ use SilverStripe\Dev\SapphireTest;
class Upload_Validator class Upload_Validator
{ {
use Injectable; use Injectable;
/** /**
* Contains a list of the max file sizes shared by * Contains a list of the max file sizes shared by

View File

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

View File

@ -25,156 +25,168 @@ use SilverStripe\Core\Object;
* <code> * <code>
* class ExampleForm_Controller extends Page_Controller { * class ExampleForm_Controller extends Page_Controller {
* *
* function Form() { * function Form() {
* $fields = new FieldList( * $fields = new FieldList(
* new TextField('MyName'), * new TextField('MyName'),
* new FileField('MyFile') * new FileField('MyFile')
* ); * );
* $actions = new FieldList( * $actions = new FieldList(
* new FormAction('doUpload', 'Upload file') * new FormAction('doUpload', 'Upload file')
* ); * );
* $validator = new RequiredFields(array('MyName', 'MyFile')); * $validator = new RequiredFields(array('MyName', 'MyFile'));
* *
* return new Form($this, 'Form', $fields, $actions, $validator); * return new Form($this, 'Form', $fields, $actions, $validator);
* } * }
* *
* function doUpload($data, $form) { * function doUpload($data, $form) {
* $file = $data['MyFile']; * $file = $data['MyFile'];
* $content = file_get_contents($file['tmp_name']); * $content = file_get_contents($file['tmp_name']);
* // ... process content * // ... process content
* } * }
* } * }
* </code> * </code>
*/ */
class FileField extends FormField { class FileField extends FormField
use UploadReceiver; {
use UploadReceiver;
/** /**
* Flag to automatically determine and save a has_one-relationship * Flag to automatically determine and save a has_one-relationship
* on the saved record (e.g. a "Player" has_one "PlayerImage" would * on the saved record (e.g. a "Player" has_one "PlayerImage" would
* trigger saving the ID of newly created file into "PlayerImageID" * trigger saving the ID of newly created file into "PlayerImageID"
* on the record). * on the record).
* *
* @var boolean * @var boolean
*/ */
protected $relationAutoSetting = true; protected $relationAutoSetting = true;
/** /**
* Create a new file field. * Create a new file field.
* *
* @param string $name The internal field name, passed to forms. * @param string $name The internal field name, passed to forms.
* @param string $title The field label. * @param string $title The field label.
* @param int $value The value of the field. * @param int $value The value of the field.
*/ */
public function __construct($name, $title = null, $value = null) { public function __construct($name, $title = null, $value = null)
$this->constructUploadReceiver(); {
parent::__construct($name, $title, $value); $this->constructUploadReceiver();
} parent::__construct($name, $title, $value);
}
/** /**
* @param array $properties * @param array $properties
* @return string * @return string
*/ */
public function Field($properties = array()) { public function Field($properties = array())
$properties = array_merge($properties, array( {
'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize() $properties = array_merge($properties, array(
)); 'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize()
));
return parent::Field($properties); return parent::Field($properties);
} }
public function getAttributes() { public function getAttributes()
return array_merge( {
parent::getAttributes(), return array_merge(
array('type' => 'file') parent::getAttributes(),
); array('type' => 'file')
} );
}
/** /**
* @param DataObject|DataObjectInterface $record * @param DataObject|DataObjectInterface $record
*/ */
public function saveInto(DataObjectInterface $record) { public function saveInto(DataObjectInterface $record)
if(!isset($_FILES[$this->name])) { {
return; if (!isset($_FILES[$this->name])) {
} return;
}
$fileClass = File::get_class_for_file_extension( $fileClass = File::get_class_for_file_extension(
File::get_file_extension($_FILES[$this->name]['name']) File::get_file_extension($_FILES[$this->name]['name'])
); );
/** @var File $file */ /** @var File $file */
if($this->relationAutoSetting) { if ($this->relationAutoSetting) {
// assume that the file is connected via a has-one // assume that the file is connected via a has-one
$objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name); $objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name);
if($objectClass === File::class || empty($objectClass)) { if ($objectClass === File::class || empty($objectClass)) {
// Create object of the appropriate file class // Create object of the appropriate file class
$file = Object::create($fileClass); $file = Object::create($fileClass);
} else { } else {
// try to create a file matching the relation // try to create a file matching the relation
$file = Object::create($objectClass); $file = Object::create($objectClass);
} }
} else if($record instanceof File) { } elseif ($record instanceof File) {
$file = $record; $file = $record;
} else { } else {
$file = Object::create($fileClass); $file = Object::create($fileClass);
} }
$this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName()); $this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName());
if($this->upload->isError()) { if ($this->upload->isError()) {
return; return;
} }
if($this->relationAutoSetting) { if ($this->relationAutoSetting) {
if (empty($objectClass)) { if (empty($objectClass)) {
return; return;
} }
$file = $this->upload->getFile(); $file = $this->upload->getFile();
$record->{$this->name . 'ID'} = $file->ID; $record->{$this->name . 'ID'} = $file->ID;
} }
} }
public function Value() { public function Value()
return isset($_FILES[$this->getName()]) ? $_FILES[$this->getName()] : null; {
} return isset($_FILES[$this->getName()]) ? $_FILES[$this->getName()] : null;
}
public function validate($validator) { public function validate($validator)
if(!isset($_FILES[$this->name])) return true; {
if (!isset($_FILES[$this->name])) {
return true;
}
$tmpFile = $_FILES[$this->name]; $tmpFile = $_FILES[$this->name];
$valid = $this->upload->validate($tmpFile); $valid = $this->upload->validate($tmpFile);
if(!$valid) { if (!$valid) {
$errors = $this->upload->getErrors(); $errors = $this->upload->getErrors();
if($errors) foreach($errors as $error) { if ($errors) {
$validator->validationError($this->name, $error, "validation"); foreach ($errors as $error) {
} $validator->validationError($this->name, $error, "validation");
return false; }
} }
return false;
}
return true; return true;
} }
/** /**
* Set if relation can be automatically assigned to the underlying dataobject * Set if relation can be automatically assigned to the underlying dataobject
* *
* @param bool $auto * @param bool $auto
* @return $this * @return $this
*/ */
public function setRelationAutoSetting($auto) { public function setRelationAutoSetting($auto)
$this->relationAutoSetting = $auto; {
return $this; $this->relationAutoSetting = $auto;
} return $this;
}
/**
* Check if relation can be automatically assigned to the underlying dataobject
*
* @return bool
*/
public function getRelationAutoSetting() {
return $this->relationAutoSetting;
}
/**
* Check if relation can be automatically assigned to the underlying dataobject
*
* @return bool
*/
public function getRelationAutoSetting()
{
return $this->relationAutoSetting;
}
} }

View File

@ -29,373 +29,388 @@ use SilverStripe\ORM\ValidationException;
* *
* @mixin FormField * @mixin FormField
*/ */
trait FileUploadReceiver { trait FileUploadReceiver
use UploadReceiver; {
use UploadReceiver;
/** /**
* Flag to automatically determine and save a has_one-relationship * Flag to automatically determine and save a has_one-relationship
* on the saved record (e.g. a "Player" has_one "PlayerImage" would * on the saved record (e.g. a "Player" has_one "PlayerImage" would
* trigger saving the ID of newly created file into "PlayerImageID" * trigger saving the ID of newly created file into "PlayerImageID"
* on the record). * on the record).
* *
* @var boolean * @var boolean
*/ */
public $relationAutoSetting = true; public $relationAutoSetting = true;
/** /**
* Parent data record. Will be infered from parent form or controller if blank. * Parent data record. Will be infered from parent form or controller if blank.
* *
* @var DataObject * @var DataObject
*/ */
protected $record; protected $record;
/** /**
* Items loaded into this field. May be a RelationList, or any other SS_List * Items loaded into this field. May be a RelationList, or any other SS_List
* *
* @var SS_List * @var SS_List
*/ */
protected $items; protected $items;
protected function constructFileUploadReceiver() { protected function constructFileUploadReceiver()
$this->constructUploadReceiver(); {
} $this->constructUploadReceiver();
}
/** /**
* Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File)
* *
* @param DataObject $record * @param DataObject $record
* @return $this * @return $this
*/ */
public function setRecord($record) { public function setRecord($record)
$this->record = $record; {
return $this; $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() or Form->Controller()->data() * 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() or Form->Controller()->data()
* @return DataObject *
*/ * @return DataObject
public function getRecord() { */
if ($this->record) { public function getRecord()
return $this->record; {
} if ($this->record) {
if (!$this->getForm()) { return $this->record;
return null; }
} if (!$this->getForm()) {
return null;
}
// Get record from form // Get record from form
$record = $this->getForm()->getRecord(); $record = $this->getForm()->getRecord();
if ($record && ($record instanceof DataObject)) { if ($record && ($record instanceof DataObject)) {
$this->record = $record; $this->record = $record;
return $record; return $record;
} }
// Get record from controller // Get record from controller
$controller = $this->getForm()->getController(); $controller = $this->getForm()->getController();
if ($controller if ($controller
&& $controller->hasMethod('data') && $controller->hasMethod('data')
&& ($record = $controller->data()) && ($record = $controller->data())
&& ($record instanceof DataObject) && ($record instanceof DataObject)
) { ) {
$this->record = $record; $this->record = $record;
return $record; return $record;
} }
return null; return null;
} }
/** /**
* Loads the related record values into this field. This can be uploaded * Loads the related record values into this field. This can be uploaded
* in one of three ways: * in one of three ways:
* *
* - By passing in a list of file IDs in the $value parameter (an array with a single * - By passing in a list of file IDs in the $value parameter (an array with a single
* key 'Files', with the value being the actual array of IDs). * key 'Files', with the value being the actual array of IDs).
* - By passing in an explicit list of File objects in the $record parameter, and * - By passing in an explicit list of File objects in the $record parameter, and
* leaving $value blank. * leaving $value blank.
* - By passing in a dataobject in the $record parameter, from which file objects * - By passing in a dataobject in the $record parameter, from which file objects
* will be extracting using the field name as the relation field. * will be extracting using the field name as the relation field.
* *
* Each of these methods will update both the items (list of File objects) and the * Each of these methods will update both the items (list of File objects) and the
* field value (list of file ID values). * field value (list of file ID values).
* *
* @param array $value Array of submitted form data, if submitting from a form * @param array $value Array of submitted form data, if submitting from a form
* @param array|DataObject|SS_List $record Full source record, either as a DataObject, * @param array|DataObject|SS_List $record Full source record, either as a DataObject,
* SS_List of items, or an array of submitted form data * SS_List of items, or an array of submitted form data
* @return $this Self reference * @return $this Self reference
* @throws ValidationException * @throws ValidationException
*/ */
public function setValue($value, $record = null) { public function setValue($value, $record = null)
{
// If we're not passed a value directly, we can attempt to infer the field // If we're not passed a value directly, we can attempt to infer the field
// value from the second parameter by inspecting its relations // value from the second parameter by inspecting its relations
$items = new ArrayList(); $items = new ArrayList();
// Determine format of presented data // Determine format of presented data
if(empty($value) && $record) { if (empty($value) && $record) {
// If a record is given as a second parameter, but no submitted values, // If a record is given as a second parameter, but no submitted values,
// then we should inspect this instead for the form values // then we should inspect this instead for the form values
if(($record instanceof DataObject) && $record->hasMethod($this->getName())) { if (($record instanceof DataObject) && $record->hasMethod($this->getName())) {
// If given a dataobject use reflection to extract details // If given a dataobject use reflection to extract details
$data = $record->{$this->getName()}(); $data = $record->{$this->getName()}();
if($data instanceof DataObject) { if ($data instanceof DataObject) {
// If has_one, add sole item to default list // If has_one, add sole item to default list
$items->push($data); $items->push($data);
} elseif($data instanceof SS_List) { } elseif ($data instanceof SS_List) {
// For many_many and has_many relations we can use the relation list directly // For many_many and has_many relations we can use the relation list directly
$items = $data; $items = $data;
} }
} elseif($record instanceof SS_List) { } elseif ($record instanceof SS_List) {
// If directly passing a list then save the items directly // If directly passing a list then save the items directly
$items = $record; $items = $record;
} }
} elseif(!empty($value['Files'])) { } elseif (!empty($value['Files'])) {
// If value is given as an array (such as a posted form), extract File IDs from this // If value is given as an array (such as a posted form), extract File IDs from this
$class = $this->getRelationAutosetClass(); $class = $this->getRelationAutosetClass();
$items = DataObject::get($class)->byIDs($value['Files']); $items = DataObject::get($class)->byIDs($value['Files']);
} }
// If javascript is disabled, direct file upload (non-html5 style) can // If javascript is disabled, direct file upload (non-html5 style) can
// trigger a single or multiple file submission. Note that this may be // trigger a single or multiple file submission. Note that this may be
// included in addition to re-submitted File IDs as above, so these // included in addition to re-submitted File IDs as above, so these
// should be added to the list instead of operated on independently. // should be added to the list instead of operated on independently.
if($uploadedFiles = $this->extractUploadedFileData($value)) { if ($uploadedFiles = $this->extractUploadedFileData($value)) {
foreach($uploadedFiles as $tempFile) { foreach ($uploadedFiles as $tempFile) {
$file = $this->saveTemporaryFile($tempFile, $error); $file = $this->saveTemporaryFile($tempFile, $error);
if($file) { if ($file) {
$items->add($file); $items->add($file);
} else { } else {
throw new ValidationException($error); throw new ValidationException($error);
} }
} }
} }
// Filter items by what's allowed to be viewed // Filter items by what's allowed to be viewed
$filteredItems = new ArrayList(); $filteredItems = new ArrayList();
$fileIDs = array(); $fileIDs = array();
foreach($items as $file) { foreach ($items as $file) {
if($file->exists() && $file->canView()) { if ($file->exists() && $file->canView()) {
$filteredItems->push($file); $filteredItems->push($file);
$fileIDs[] = $file->ID; $fileIDs[] = $file->ID;
} }
} }
// Filter and cache updated item list // Filter and cache updated item list
$this->items = $filteredItems; $this->items = $filteredItems;
// Same format as posted form values for this field. Also ensures that // Same format as posted form values for this field. Also ensures that
// $this->setValue($this->getValue()); is non-destructive // $this->setValue($this->getValue()); is non-destructive
$value = $fileIDs ? array('Files' => $fileIDs) : null; $value = $fileIDs ? array('Files' => $fileIDs) : null;
// Set value using parent // Set value using parent
parent::setValue($value, $record); parent::setValue($value, $record);
return $this; return $this;
} }
/** /**
* Sets the items assigned to this field as an SS_List of File objects. * Sets the items assigned to this field as an SS_List of File objects.
* Calling setItems will also update the value of this field, as well as * Calling setItems will also update the value of this field, as well as
* updating the internal list of File items. * updating the internal list of File items.
* *
* @param SS_List $items * @param SS_List $items
* @return $this self reference * @return $this self reference
*/ */
public function setItems(SS_List $items) { public function setItems(SS_List $items)
return $this->setValue(null, $items); {
} return $this->setValue(null, $items);
}
/** /**
* Retrieves the current list of files * Retrieves the current list of files
* *
* @return SS_List|File[] * @return SS_List|File[]
*/ */
public function getItems() { public function getItems()
return $this->items ? $this->items : new ArrayList(); {
} return $this->items ? $this->items : new ArrayList();
}
/** /**
* Retrieves the list of selected file IDs * Retrieves the list of selected file IDs
* *
* @return array * @return array
*/ */
public function getItemIDs() { public function getItemIDs()
$value = $this->Value(); {
return empty($value['Files']) ? array() : $value['Files']; $value = $this->Value();
} return empty($value['Files']) ? array() : $value['Files'];
}
public function Value() { public function Value()
// Re-override FileField Value to use data value {
return $this->dataValue(); // Re-override FileField Value to use data value
} return $this->dataValue();
}
/** /**
* @param DataObject|DataObjectInterface $record * @param DataObject|DataObjectInterface $record
* @return $this * @return $this
*/ */
public function saveInto(DataObjectInterface $record) { public function saveInto(DataObjectInterface $record)
// Check required relation details are available {
$fieldname = $this->getName(); // Check required relation details are available
if(!$fieldname) { $fieldname = $this->getName();
return $this; if (!$fieldname) {
} return $this;
}
// Get details to save // Get details to save
$idList = $this->getItemIDs(); $idList = $this->getItemIDs();
// Check type of relation // Check type of relation
$relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null;
if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) { if ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) {
// has_many or many_many // has_many or many_many
$relation->setByIDList($idList); $relation->setByIDList($idList);
} elseif($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) { } elseif ($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) {
// Assign has_one ID // Assign has_one ID
$id = $idList ? reset($idList) : 0; $id = $idList ? reset($idList) : 0;
$record->{"{$fieldname}ID"} = $id; $record->{"{$fieldname}ID"} = $id;
// Polymorphic asignment // Polymorphic asignment
if ($class === DataObject::class) { if ($class === DataObject::class) {
$file = $id ? File::get()->byID($id) : null; $file = $id ? File::get()->byID($id) : null;
$fileClass = $file ? get_class($file) : File::class; $fileClass = $file ? get_class($file) : File::class;
$record->{"{$fieldname}Class"} = $id ? $fileClass : null; $record->{"{$fieldname}Class"} = $id ? $fileClass : null;
} }
} }
return $this; return $this;
} }
/** /**
* Loads the temporary file data into a File object * Loads the temporary file data into a File object
* *
* @param array $tmpFile Temporary file data * @param array $tmpFile Temporary file data
* @param string $error Error message * @param string $error Error message
* @return AssetContainer File object, or null if error * @return AssetContainer File object, or null if error
*/ */
protected function saveTemporaryFile($tmpFile, &$error = null) { protected function saveTemporaryFile($tmpFile, &$error = null)
// Determine container object {
$error = null; // Determine container object
$fileObject = null; $error = null;
$fileObject = null;
if (empty($tmpFile)) { if (empty($tmpFile)) {
$error = _t('UploadField.FIELDNOTSET', 'File information not found'); $error = _t('UploadField.FIELDNOTSET', 'File information not found');
return null; return null;
} }
if($tmpFile['error']) { if ($tmpFile['error']) {
$error = $tmpFile['error']; $error = $tmpFile['error'];
return null; return null;
} }
// Search for relations that can hold the uploaded files, but don't fallback // Search for relations that can hold the uploaded files, but don't fallback
// to default if there is no automatic relation // to default if there is no automatic relation
if ($relationClass = $this->getRelationAutosetClass(null)) { if ($relationClass = $this->getRelationAutosetClass(null)) {
// Allow File to be subclassed // Allow File to be subclassed
if($relationClass === File::class && isset($tmpFile['name'])) { if ($relationClass === File::class && isset($tmpFile['name'])) {
$relationClass = File::get_class_for_file_extension( $relationClass = File::get_class_for_file_extension(
File::get_file_extension($tmpFile['name']) File::get_file_extension($tmpFile['name'])
); );
} }
// Create new object explicitly. Otherwise rely on Upload::load to choose the class. // Create new object explicitly. Otherwise rely on Upload::load to choose the class.
$fileObject = Object::create($relationClass); $fileObject = Object::create($relationClass);
if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) { if (! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) {
throw new InvalidArgumentException("Invalid asset container $relationClass"); throw new InvalidArgumentException("Invalid asset container $relationClass");
} }
} }
// Get the uploaded file into a new file object. // Get the uploaded file into a new file object.
try { try {
$this->getUpload()->loadIntoFile($tmpFile, $fileObject, $this->getFolderName()); $this->getUpload()->loadIntoFile($tmpFile, $fileObject, $this->getFolderName());
} catch (Exception $e) { } catch (Exception $e) {
// we shouldn't get an error here, but just in case // we shouldn't get an error here, but just in case
$error = $e->getMessage(); $error = $e->getMessage();
return null; return null;
} }
// Check if upload field has an error // Check if upload field has an error
if ($this->getUpload()->isError()) { if ($this->getUpload()->isError()) {
$error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors()); $error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors());
return null; return null;
} }
// return file // return file
return $this->getUpload()->getFile(); return $this->getUpload()->getFile();
} }
/** /**
* Gets the foreign class that needs to be created, or 'File' as default if there * Gets the foreign class that needs to be created, or 'File' as default if there
* is no relationship, or it cannot be determined. * is no relationship, or it cannot be determined.
* *
* @param string $default Default value to return if no value could be calculated * @param string $default Default value to return if no value could be calculated
* @return string Foreign class name. * @return string Foreign class name.
*/ */
public function getRelationAutosetClass($default = File::class) { public function getRelationAutosetClass($default = File::class)
// Don't autodetermine relation if no relationship between parent record {
if(!$this->getRelationAutoSetting()) { // Don't autodetermine relation if no relationship between parent record
return $default; if (!$this->getRelationAutoSetting()) {
} return $default;
}
// Check record and name // Check record and name
$name = $this->getName(); $name = $this->getName();
$record = $this->getRecord(); $record = $this->getRecord();
if(empty($name) || empty($record)) { if (empty($name) || empty($record)) {
return $default; return $default;
} else { } else {
$class = $record->getRelationClass($name); $class = $record->getRelationClass($name);
return empty($class) ? $default : $class; return empty($class) ? $default : $class;
} }
} }
/** /**
* Set if relation can be automatically assigned to the underlying dataobject * Set if relation can be automatically assigned to the underlying dataobject
* *
* @param bool $auto * @param bool $auto
* @return $this * @return $this
*/ */
public function setRelationAutoSetting($auto) { public function setRelationAutoSetting($auto)
$this->relationAutoSetting = $auto; {
return $this; $this->relationAutoSetting = $auto;
} return $this;
}
/** /**
* Check if relation can be automatically assigned to the underlying dataobject * Check if relation can be automatically assigned to the underlying dataobject
* *
* @return bool * @return bool
*/ */
public function getRelationAutoSetting() { public function getRelationAutoSetting()
return $this->relationAutoSetting; {
} return $this->relationAutoSetting;
}
/** /**
* Given an array of post variables, extract all temporary file data into an array * Given an array of post variables, extract all temporary file data into an array
* *
* @param array $postVars Array of posted form data * @param array $postVars Array of posted form data
* @return array List of temporary file data * @return array List of temporary file data
*/ */
protected function extractUploadedFileData($postVars) { protected function extractUploadedFileData($postVars)
// Note: Format of posted file parameters in php is a feature of using {
// <input name='{$Name}[Uploads][]' /> for multiple file uploads // Note: Format of posted file parameters in php is a feature of using
$tmpFiles = array(); // <input name='{$Name}[Uploads][]' /> for multiple file uploads
if( !empty($postVars['tmp_name']) $tmpFiles = array();
&& is_array($postVars['tmp_name']) if (!empty($postVars['tmp_name'])
&& !empty($postVars['tmp_name']['Uploads']) && is_array($postVars['tmp_name'])
) { && !empty($postVars['tmp_name']['Uploads'])
for($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) { ) {
// Skip if "empty" file for ($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) {
if(empty($postVars['tmp_name']['Uploads'][$i])) { // Skip if "empty" file
continue; if (empty($postVars['tmp_name']['Uploads'][$i])) {
} continue;
$tmpFile = array(); }
foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) { $tmpFile = array();
$tmpFile[$field] = $postVars[$field]['Uploads'][$i]; foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $field) {
} $tmpFile[$field] = $postVars[$field]['Uploads'][$i];
$tmpFiles[] = $tmpFile; }
} $tmpFiles[] = $tmpFile;
} elseif(!empty($postVars['tmp_name'])) { }
// Fallback to allow single file uploads (method used by AssetUploadField) } elseif (!empty($postVars['tmp_name'])) {
$tmpFiles[] = $postVars; // Fallback to allow single file uploads (method used by AssetUploadField)
} $tmpFiles[] = $postVars;
}
return $tmpFiles; return $tmpFiles;
} }
} }

View File

@ -45,1000 +45,1070 @@ use Exception;
* Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context, * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context,
* since the required frontend dependencies are included through CMS bundling. * since the required frontend dependencies are included through CMS bundling.
*/ */
class UploadField extends FormField { class UploadField extends FormField
use FileUploadReceiver; {
use FileUploadReceiver;
/** /**
* @var array * @var array
*/ */
private static $allowed_actions = array( private static $allowed_actions = array(
'upload', 'upload',
'attach', 'attach',
'handleItem', 'handleItem',
'handleSelect', 'handleSelect',
'fileexists' 'fileexists'
); );
/** /**
* @var array * @var array
*/ */
private static $url_handlers = array( private static $url_handlers = array(
'item/$ID' => 'handleItem', 'item/$ID' => 'handleItem',
'select' => 'handleSelect', 'select' => 'handleSelect',
'$Action!' => '$Action', '$Action!' => '$Action',
); );
/** /**
* Template to use for the file button widget * Template to use for the file button widget
* *
* @var string * @var string
*/ */
protected $templateFileButtons = null; protected $templateFileButtons = null;
/** /**
* Template to use for the edit form * Template to use for the edit form
* *
* @var string * @var string
*/ */
protected $templateFileEdit = null; protected $templateFileEdit = null;
/** /**
* Config for this field used in the front-end javascript * Config for this field used in the front-end javascript
* (will be merged into the config of the javascript file upload plugin). * (will be merged into the config of the javascript file upload plugin).
* *
* @var array * @var array
*/ */
protected $ufConfig = array(); protected $ufConfig = array();
/** /**
* Front end config defaults * Front end config defaults
* *
* @config * @config
* @var array * @var array
*/ */
private static $defaultConfig = array( private static $defaultConfig = array(
/** /**
* Automatically upload the file once selected * Automatically upload the file once selected
* *
* @var boolean * @var boolean
*/ */
'autoUpload' => true, 'autoUpload' => true,
/** /**
* Restriction on number of files that may be set for this field. Set to null to allow * Restriction on number of files that may be set for this field. Set to null to allow
* unlimited. If record has a has_one and allowedMaxFileNumber is null, it will be set to 1. * unlimited. If record has a has_one and allowedMaxFileNumber is null, it will be set to 1.
* The resulting value will be set to maxNumberOfFiles * The resulting value will be set to maxNumberOfFiles
* *
* @var integer * @var integer
*/ */
'allowedMaxFileNumber' => null, 'allowedMaxFileNumber' => null,
/** /**
* Can the user upload new files, or just select from existing files. * Can the user upload new files, or just select from existing files.
* String values are interpreted as permission codes. * String values are interpreted as permission codes.
* *
* @var boolean|string * @var boolean|string
*/ */
'canUpload' => true, 'canUpload' => true,
/** /**
* Can the user attach files from the assets archive on the site? * Can the user attach files from the assets archive on the site?
* String values are interpreted as permission codes. * String values are interpreted as permission codes.
* *
* @var boolean|string * @var boolean|string
*/ */
'canAttachExisting' => "CMS_ACCESS_AssetAdmin", 'canAttachExisting' => "CMS_ACCESS_AssetAdmin",
/** /**
* Shows the target folder for new uploads in the field UI. * Shows the target folder for new uploads in the field UI.
* Disable to keep the internal filesystem structure hidden from users. * Disable to keep the internal filesystem structure hidden from users.
* *
* @var boolean|string * @var boolean|string
*/ */
'canPreviewFolder' => true, 'canPreviewFolder' => true,
/** /**
* Indicate a change event to the containing form if an upload * Indicate a change event to the containing form if an upload
* or file edit/delete was performed. * or file edit/delete was performed.
* *
* @var boolean * @var boolean
*/ */
'changeDetection' => true, 'changeDetection' => true,
/** /**
* Maximum width of the preview thumbnail * Maximum width of the preview thumbnail
* *
* @var integer * @var integer
*/ */
'previewMaxWidth' => 80, 'previewMaxWidth' => 80,
/** /**
* Maximum height of the preview thumbnail * Maximum height of the preview thumbnail
* *
* @var integer * @var integer
*/ */
'previewMaxHeight' => 60, 'previewMaxHeight' => 60,
/** /**
* javascript template used to display uploading files * javascript template used to display uploading files
* *
* @see javascript/UploadField_uploadtemplate.js * @see javascript/UploadField_uploadtemplate.js
* @var string * @var string
*/ */
'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate',
/** /**
* javascript template used to display already uploaded files * javascript template used to display already uploaded files
* *
* @see javascript/UploadField_downloadtemplate.js * @see javascript/UploadField_downloadtemplate.js
* @var string * @var string
*/ */
'downloadTemplateName' => 'ss-uploadfield-downloadtemplate', 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate',
/** /**
* Show a warning when overwriting a file. * Show a warning when overwriting a file.
* This requires Upload->replaceFile config to be set to true, otherwise * This requires Upload->replaceFile config to be set to true, otherwise
* files will be renamed instead of overwritten * files will be renamed instead of overwritten
* *
* @see Upload * @see Upload
* @var boolean * @var boolean
*/ */
'overwriteWarning' => true 'overwriteWarning' => true
); );
/** /**
* @var String Folder to display in "Select files" list. * @var String Folder to display in "Select files" list.
* Defaults to listing all files regardless of folder. * Defaults to listing all files regardless of folder.
* The folder path should be relative to the webroot. * The folder path should be relative to the webroot.
* See {@link FileField->folderName} to set the upload target instead. * See {@link FileField->folderName} to set the upload target instead.
* @example admin/folder/subfolder * @example admin/folder/subfolder
*/ */
protected $displayFolderName; protected $displayFolderName;
/** /**
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
* @example 'getCMSFields' * @example 'getCMSFields'
* *
* @var FieldList|string * @var FieldList|string
*/ */
protected $fileEditFields = null; protected $fileEditFields = null;
/** /**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* @example 'getCMSActions' * @example 'getCMSActions'
* *
* @var FieldList|string * @var FieldList|string
*/ */
protected $fileEditActions = null; protected $fileEditActions = null;
/** /**
* Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
* @example 'getCMSValidator' * @example 'getCMSValidator'
* *
* @var RequiredFields|string * @var RequiredFields|string
*/ */
protected $fileEditValidator = null; protected $fileEditValidator = null;
/** /**
* Construct a new UploadField instance * Construct a new UploadField instance
* *
* @param string $name The internal field name, passed to forms. * @param string $name The internal field name, passed to forms.
* @param string $title The field label. * @param string $title The field label.
* @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on * @param SS_List $items If no items are defined, the field will try to auto-detect an existing relation on
* @link $record}, with the same name as the field name. * @link $record}, with the same name as the field name.
*/ */
public function __construct($name, $title = null, SS_List $items = null) { public function __construct($name, $title = null, SS_List $items = null)
// TODO thats the first thing that came to my head, feel free to change it {
$this->addExtraClass('ss-upload'); // class, used by js // TODO thats the first thing that came to my head, feel free to change it
$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only $this->addExtraClass('ss-upload'); // class, used by js
$this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only
$this->ufConfig = self::config()->defaultConfig; $this->ufConfig = self::config()->defaultConfig;
$this->constructFileUploadReceiver(); $this->constructFileUploadReceiver();
parent::__construct($name, $title); parent::__construct($name, $title);
if ($items) { if ($items) {
$this->setItems($items); $this->setItems($items);
} }
} }
/** /**
* Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension)
* *
* @param string $template * @param string $template
* @return $this * @return $this
*/ */
public function setTemplateFileButtons($template) { public function setTemplateFileButtons($template)
$this->templateFileButtons = $template; {
return $this; $this->templateFileButtons = $template;
} return $this;
}
/**
* @return string /**
*/ * @return string
public function getTemplateFileButtons() { */
return $this->_templates($this->templateFileButtons, '_FileButtons'); public function getTemplateFileButtons()
} {
return $this->_templates($this->templateFileButtons, '_FileButtons');
/** }
* Set name of template used for the edit (inline & popup) of a file file (without path or extension)
* /**
* @param string $template * Set name of template used for the edit (inline & popup) of a file file (without path or extension)
* @return $this *
*/ * @param string $template
public function setTemplateFileEdit($template) { * @return $this
$this->templateFileEdit = $template; */
return $this; public function setTemplateFileEdit($template)
} {
$this->templateFileEdit = $template;
/** return $this;
* @return string }
*/
public function getTemplateFileEdit() { /**
return $this->_templates($this->templateFileEdit, '_FileEdit'); * @return string
} */
public function getTemplateFileEdit()
/** {
* Determine if the target folder for new uploads in is visible the field UI. return $this->_templates($this->templateFileEdit, '_FileEdit');
* }
* @return boolean
*/ /**
public function canPreviewFolder() { * Determine if the target folder for new uploads in is visible the field UI.
if(!$this->isActive()) return false; *
$can = $this->getConfig('canPreviewFolder'); * @return boolean
return (is_bool($can)) ? $can : Permission::check($can); */
} public function canPreviewFolder()
{
/** if (!$this->isActive()) {
* Determine if the target folder for new uploads in is visible the field UI. return false;
* Disable to keep the internal filesystem structure hidden from users. }
* $can = $this->getConfig('canPreviewFolder');
* @param boolean|string $canPreviewFolder Either a boolean flag, or a return (is_bool($can)) ? $can : Permission::check($can);
* required permission code }
* @return UploadField Self reference
*/ /**
public function setCanPreviewFolder($canPreviewFolder) { * Determine if the target folder for new uploads in is visible the field UI.
return $this->setConfig('canPreviewFolder', $canPreviewFolder); * Disable to keep the internal filesystem structure hidden from users.
} *
* @param boolean|string $canPreviewFolder Either a boolean flag, or a
/** * required permission code
* Determine if the field should show a warning when overwriting a file. * @return UploadField Self reference
* This requires Upload->replaceFile config to be set to true, otherwise */
* files will be renamed instead of overwritten (although the warning will public function setCanPreviewFolder($canPreviewFolder)
* still be displayed) {
* return $this->setConfig('canPreviewFolder', $canPreviewFolder);
* @return boolean }
*/
public function getOverwriteWarning() { /**
return $this->getConfig('overwriteWarning'); * Determine if the field should show a warning when overwriting a file.
} * This requires Upload->replaceFile config to be set to true, otherwise
* files will be renamed instead of overwritten (although the warning will
/** * still be displayed)
* Determine if the field should show a warning when overwriting a file. *
* This requires Upload->replaceFile config to be set to true, otherwise * @return boolean
* files will be renamed instead of overwritten (although the warning will */
* still be displayed) public function getOverwriteWarning()
* {
* @param boolean $overwriteWarning return $this->getConfig('overwriteWarning');
* @return UploadField Self reference }
*/
public function setOverwriteWarning($overwriteWarning) { /**
return $this->setConfig('overwriteWarning', $overwriteWarning); * Determine if the field should show a warning when overwriting a file.
} * This requires Upload->replaceFile config to be set to true, otherwise
* files will be renamed instead of overwritten (although the warning will
/** * still be displayed)
* @param string $name *
* @return $this * @param boolean $overwriteWarning
*/ * @return UploadField Self reference
public function setDisplayFolderName($name) { */
$this->displayFolderName = $name; public function setOverwriteWarning($overwriteWarning)
return $this; {
} return $this->setConfig('overwriteWarning', $overwriteWarning);
}
/**
* @return String /**
*/ * @param string $name
public function getDisplayFolderName() { * @return $this
return $this->displayFolderName; */
} public function setDisplayFolderName($name)
{
$this->displayFolderName = $name;
return $this;
/** }
* Retrieves a customised list of all File records to ensure they are
* properly viewable when rendered in the field template. /**
* * @return String
* @return SS_List[ViewableData_Customised] */
*/ public function getDisplayFolderName()
public function getCustomisedItems() { {
$customised = new ArrayList(); return $this->displayFolderName;
foreach($this->getItems() as $file) { }
$customised->push($this->customiseFile($file));
}
return $customised;
} /**
* Retrieves a customised list of all File records to ensure they are
/** * properly viewable when rendered in the field template.
* Customises a file with additional details suitable for rendering in the *
* UploadField.ss template * @return SS_List[ViewableData_Customised]
* */
* @param ViewableData|AssetContainer $file public function getCustomisedItems()
* @return ViewableData_Customised {
*/ $customised = new ArrayList();
protected function customiseFile(AssetContainer $file) { foreach ($this->getItems() as $file) {
$file = $file->customise(array( $customised->push($this->customiseFile($file));
'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file), }
'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(), return $customised;
'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(), }
'UploadField' => $this
)); /**
// we do this in a second customise to have the access to the previous customisations * Customises a file with additional details suitable for rendering in the
return $file->customise(array( * UploadField.ss template
'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons()) *
)); * @param ViewableData|AssetContainer $file
} * @return ViewableData_Customised
*/
/** protected function customiseFile(AssetContainer $file)
* Assign a front-end config variable for the upload field {
* $file = $file->customise(array(
* @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available 'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file),
* 'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(),
* @param string $key 'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(),
* @param mixed $val 'UploadField' => $this
* @return UploadField self reference ));
*/ // we do this in a second customise to have the access to the previous customisations
public function setConfig($key, $val) { return $file->customise(array(
$this->ufConfig[$key] = $val; 'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons())
return $this; ));
} }
/** /**
* Gets a front-end config variable for the upload field * 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 * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
* *
* @param string $key * @param string $key
* @return mixed * @param mixed $val
*/ * @return UploadField self reference
public function getConfig($key) { */
if(!isset($this->ufConfig[$key])) return null; public function setConfig($key, $val)
return $this->ufConfig[$key]; {
} $this->ufConfig[$key] = $val;
return $this;
/** }
* Determine if the field should automatically upload the file.
* /**
* @return boolean * Gets a front-end config variable for the upload field
*/ *
public function getAutoUpload() { * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available
return $this->getConfig('autoUpload'); *
} * @param string $key
* @return mixed
/** */
* Determine if the field should automatically upload the file public function getConfig($key)
* {
* @param boolean $autoUpload if (!isset($this->ufConfig[$key])) {
* @return UploadField Self reference return null;
*/ }
public function setAutoUpload($autoUpload) { return $this->ufConfig[$key];
return $this->setConfig('autoUpload', $autoUpload); }
}
/**
/** * Determine if the field should automatically upload the file.
* Determine maximum number of files allowed to be attached *
* Defaults to 1 for has_one and null (unlimited) for * @return boolean
* many_many and has_many relations. */
* public function getAutoUpload()
* @return integer|null Maximum limit, or null for no limit {
*/ return $this->getConfig('autoUpload');
public function getAllowedMaxFileNumber() { }
$allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
/**
// if there is a has_one relation with that name on the record and * Determine if the field should automatically upload the file
// allowedMaxFileNumber has not been set, it's wanted to be 1 *
if(empty($allowedMaxFileNumber)) { * @param boolean $autoUpload
$record = $this->getRecord(); * @return UploadField Self reference
$name = $this->getName(); */
if($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) { public function setAutoUpload($autoUpload)
return 1; // Default for has_one {
} else { return $this->setConfig('autoUpload', $autoUpload);
return null; // Default for has_many and many_many }
}
} else { /**
return $allowedMaxFileNumber; * Determine maximum number of files allowed to be attached
} * Defaults to 1 for has_one and null (unlimited) for
} * many_many and has_many relations.
*
/** * @return integer|null Maximum limit, or null for no limit
* Determine maximum number of files allowed to be attached. */
* public function getAllowedMaxFileNumber()
* @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited {
* @return UploadField Self reference $allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber');
*/
public function setAllowedMaxFileNumber($allowedMaxFileNumber) { // if there is a has_one relation with that name on the record and
return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber); // allowedMaxFileNumber has not been set, it's wanted to be 1
} if (empty($allowedMaxFileNumber)) {
$record = $this->getRecord();
/** $name = $this->getName();
* Determine if the user has permission to upload. if ($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) {
* return 1; // Default for has_one
* @return boolean } else {
*/ return null; // Default for has_many and many_many
public function canUpload() { }
if(!$this->isActive()) return false; } else {
$can = $this->getConfig('canUpload'); return $allowedMaxFileNumber;
return (is_bool($can)) ? $can : Permission::check($can); }
} }
/** /**
* Specify whether the user can upload files. * Determine maximum number of files allowed to be attached.
* String values will be treated as required permission codes *
* * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited
* @param boolean|string $canUpload Either a boolean flag, or a required * @return UploadField Self reference
* permission code */
* @return UploadField Self reference public function setAllowedMaxFileNumber($allowedMaxFileNumber)
*/ {
public function setCanUpload($canUpload) { return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber);
return $this->setConfig('canUpload', $canUpload); }
}
/**
/** * Determine if the user has permission to upload.
* Determine if the user has permission to attach existing files *
* By default returns true if the user has the CMS_ACCESS_AssetAdmin permission * @return boolean
* */
* @return boolean public function canUpload()
*/ {
public function canAttachExisting() { if (!$this->isActive()) {
if(!$this->isActive()) return false; return false;
$can = $this->getConfig('canAttachExisting'); }
return (is_bool($can)) ? $can : Permission::check($can); $can = $this->getConfig('canUpload');
} return (is_bool($can)) ? $can : Permission::check($can);
}
/**
* Returns true if the field is neither readonly nor disabled /**
* * Specify whether the user can upload files.
* @return boolean * String values will be treated as required permission codes
*/ *
public function isActive() { * @param boolean|string $canUpload Either a boolean flag, or a required
return !$this->isDisabled() && !$this->isReadonly(); * permission code
} * @return UploadField Self reference
*/
/** public function setCanUpload($canUpload)
* Specify whether the user can attach existing files {
* String values will be treated as required permission codes return $this->setConfig('canUpload', $canUpload);
* }
* @param boolean|string $canAttachExisting Either a boolean flag, or a
* required permission code /**
* @return UploadField Self reference * Determine if the user has permission to attach existing files
*/ * By default returns true if the user has the CMS_ACCESS_AssetAdmin permission
public function setCanAttachExisting($canAttachExisting) { *
return $this->setConfig('canAttachExisting', $canAttachExisting); * @return boolean
} */
public function canAttachExisting()
/** {
* Gets thumbnail width. Defaults to 80 if (!$this->isActive()) {
* return false;
* @return integer }
*/ $can = $this->getConfig('canAttachExisting');
public function getPreviewMaxWidth() { return (is_bool($can)) ? $can : Permission::check($can);
return $this->getConfig('previewMaxWidth'); }
}
/**
/** * Returns true if the field is neither readonly nor disabled
* @see UploadField::getPreviewMaxWidth() *
* * @return boolean
* @param integer $previewMaxWidth */
* @return UploadField Self reference public function isActive()
*/ {
public function setPreviewMaxWidth($previewMaxWidth) { return !$this->isDisabled() && !$this->isReadonly();
return $this->setConfig('previewMaxWidth', $previewMaxWidth); }
}
/**
/** * Specify whether the user can attach existing files
* Gets thumbnail height. Defaults to 60 * String values will be treated as required permission codes
* *
* @return integer * @param boolean|string $canAttachExisting Either a boolean flag, or a
*/ * required permission code
public function getPreviewMaxHeight() { * @return UploadField Self reference
return $this->getConfig('previewMaxHeight'); */
} public function setCanAttachExisting($canAttachExisting)
{
/** return $this->setConfig('canAttachExisting', $canAttachExisting);
* @see UploadField::getPreviewMaxHeight() }
*
* @param integer $previewMaxHeight /**
* @return UploadField Self reference * Gets thumbnail width. Defaults to 80
*/ *
public function setPreviewMaxHeight($previewMaxHeight) { * @return integer
return $this->setConfig('previewMaxHeight', $previewMaxHeight); */
} public function getPreviewMaxWidth()
{
/** return $this->getConfig('previewMaxWidth');
* javascript template used to display uploading files }
* Defaults to 'ss-uploadfield-uploadtemplate'
* /**
* @see javascript/UploadField_uploadtemplate.js * @see UploadField::getPreviewMaxWidth()
* @return string *
*/ * @param integer $previewMaxWidth
public function getUploadTemplateName() { * @return UploadField Self reference
return $this->getConfig('uploadTemplateName'); */
} public function setPreviewMaxWidth($previewMaxWidth)
{
/** return $this->setConfig('previewMaxWidth', $previewMaxWidth);
* @see UploadField::getUploadTemplateName() }
*
* @param string $uploadTemplateName /**
* @return UploadField Self reference * Gets thumbnail height. Defaults to 60
*/ *
public function setUploadTemplateName($uploadTemplateName) { * @return integer
return $this->setConfig('uploadTemplateName', $uploadTemplateName); */
} public function getPreviewMaxHeight()
{
/** return $this->getConfig('previewMaxHeight');
* javascript template used to display already uploaded files }
* Defaults to 'ss-downloadfield-downloadtemplate'
* /**
* @see javascript/DownloadField_downloadtemplate.js * @see UploadField::getPreviewMaxHeight()
* @return string *
*/ * @param integer $previewMaxHeight
public function getDownloadTemplateName() { * @return UploadField Self reference
return $this->getConfig('downloadTemplateName'); */
} public function setPreviewMaxHeight($previewMaxHeight)
{
/** return $this->setConfig('previewMaxHeight', $previewMaxHeight);
* @see Uploadfield::getDownloadTemplateName() }
*
* @param string $downloadTemplateName /**
* @return Uploadfield Self reference * javascript template used to display uploading files
*/ * Defaults to 'ss-uploadfield-uploadtemplate'
public function setDownloadTemplateName($downloadTemplateName) { *
return $this->setConfig('downloadTemplateName', $downloadTemplateName); * @see javascript/UploadField_uploadtemplate.js
} * @return string
*/
/** public function getUploadTemplateName()
* FieldList $fields for the EditForm {
* @example 'getCMSFields' return $this->getConfig('uploadTemplateName');
* }
* @param DataObject $file File context to generate fields for
* @return FieldList List of form fields /**
*/ * @see UploadField::getUploadTemplateName()
public function getFileEditFields(DataObject $file) { *
// Empty actions, generate default * @param string $uploadTemplateName
if(empty($this->fileEditFields)) { * @return UploadField Self reference
$fields = $file->getCMSFields(); */
// Only display main tab, to avoid overly complex interface public function setUploadTemplateName($uploadTemplateName)
if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) { {
$fields = $mainTab->Fields(); return $this->setConfig('uploadTemplateName', $uploadTemplateName);
} }
return $fields;
} /**
* javascript template used to display already uploaded files
// Fields instance * Defaults to 'ss-downloadfield-downloadtemplate'
if ($this->fileEditFields instanceof FieldList) { *
return $this->fileEditFields; * @see javascript/DownloadField_downloadtemplate.js
} * @return string
*/
// Method to call on the given file public function getDownloadTemplateName()
if($file->hasMethod($this->fileEditFields)) { {
return $file->{$this->fileEditFields}(); return $this->getConfig('downloadTemplateName');
} }
throw new InvalidArgumentException("Invalid value for UploadField::fileEditFields"); /**
} * @see Uploadfield::getDownloadTemplateName()
*
/** * @param string $downloadTemplateName
* FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm * @return Uploadfield Self reference
* @example 'getCMSFields' */
* public function setDownloadTemplateName($downloadTemplateName)
* @param FieldList|string {
* @return Uploadfield Self reference return $this->setConfig('downloadTemplateName', $downloadTemplateName);
*/ }
public function setFileEditFields($fileEditFields) {
$this->fileEditFields = $fileEditFields; /**
return $this; * FieldList $fields for the EditForm
} * @example 'getCMSFields'
*
/** * @param DataObject $file File context to generate fields for
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm * @return FieldList List of form fields
* @example 'getCMSActions' */
* public function getFileEditFields(DataObject $file)
* @param DataObject $file File context to generate form actions for {
* @return FieldList Field list containing FormAction // Empty actions, generate default
*/ if (empty($this->fileEditFields)) {
public function getFileEditActions(DataObject $file) { $fields = $file->getCMSFields();
// Empty actions, generate default // Only display main tab, to avoid overly complex interface
if(empty($this->fileEditActions)) { if ($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) {
$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save'))); $fields = $mainTab->Fields();
$saveAction->addExtraClass('ss-ui-action-constructive icon-accept'); }
return $actions; return $fields;
} }
// Actions instance // Fields instance
if ($this->fileEditActions instanceof FieldList) { if ($this->fileEditFields instanceof FieldList) {
return $this->fileEditActions; return $this->fileEditFields;
} }
// Method to call on the given file // Method to call on the given file
if($file->hasMethod($this->fileEditActions)) { if ($file->hasMethod($this->fileEditFields)) {
return $file->{$this->fileEditActions}(); return $file->{$this->fileEditFields}();
} }
throw new InvalidArgumentException("Invalid value for UploadField::fileEditActions"); throw new InvalidArgumentException("Invalid value for UploadField::fileEditFields");
} }
/** /**
* FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm
* @example 'getCMSActions' * @example 'getCMSFields'
* *
* @param FieldList|string * @param FieldList|string
* @return Uploadfield Self reference * @return Uploadfield Self reference
*/ */
public function setFileEditActions($fileEditActions) { public function setFileEditFields($fileEditFields)
$this->fileEditActions = $fileEditActions; {
return $this; $this->fileEditFields = $fileEditFields;
} return $this;
}
/**
* Determines the validator to use for the edit form /**
* @example 'getCMSValidator' * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
* * @example 'getCMSActions'
* @param DataObject $file File context to generate validator from *
* @return Validator Validator object * @param DataObject $file File context to generate form actions for
*/ * @return FieldList Field list containing FormAction
public function getFileEditValidator(DataObject $file) { */
// Empty validator public function getFileEditActions(DataObject $file)
if(empty($this->fileEditValidator)) { {
return null; // Empty actions, generate default
} if (empty($this->fileEditActions)) {
$actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save')));
// Validator instance $saveAction->addExtraClass('ss-ui-action-constructive icon-accept');
if($this->fileEditValidator instanceof Validator) { return $actions;
return $this->fileEditValidator; }
}
// Actions instance
// Method to call on the given file if ($this->fileEditActions instanceof FieldList) {
if($file->hasMethod($this->fileEditValidator)) { return $this->fileEditActions;
return $file->{$this->fileEditValidator}(); }
}
// Method to call on the given file
throw new InvalidArgumentException("Invalid value for UploadField::fileEditValidator"); if ($file->hasMethod($this->fileEditActions)) {
} return $file->{$this->fileEditActions}();
}
/**
* Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm throw new InvalidArgumentException("Invalid value for UploadField::fileEditActions");
* @example 'getCMSValidator' }
*
* @param Validator|string /**
* @return Uploadfield Self reference * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm
*/ * @example 'getCMSActions'
public function setFileEditValidator($fileEditValidator) { *
$this->fileEditValidator = $fileEditValidator; * @param FieldList|string
return $this; * @return Uploadfield Self reference
} */
public function setFileEditActions($fileEditActions)
/** {
* $this->fileEditActions = $fileEditActions;
* @param File|AssetContainer $file return $this;
* @return string URL to thumbnail }
*/
protected function getThumbnailURLForFile(AssetContainer $file) { /**
if (!$file->exists()) { * Determines the validator to use for the edit form
return null; * @example 'getCMSValidator'
} *
* @param DataObject $file File context to generate validator from
// Attempt to generate image at given size * @return Validator Validator object
$width = $this->getPreviewMaxWidth(); */
$height = $this->getPreviewMaxHeight(); public function getFileEditValidator(DataObject $file)
if ($file->hasMethod('ThumbnailURL')) { {
return $file->ThumbnailURL($width, $height); // Empty validator
} if (empty($this->fileEditValidator)) {
if ($file->hasMethod('Thumbnail')) { return null;
return $file->Thumbnail($width, $height)->getURL(); }
}
if ($file->hasMethod('Fit')) { // Validator instance
return $file->Fit($width, $height)->getURL(); if ($this->fileEditValidator instanceof Validator) {
} return $this->fileEditValidator;
}
// Check if unsized icon is available
if($file->hasMethod('getIcon')) { // Method to call on the given file
return $file->getIcon(); if ($file->hasMethod($this->fileEditValidator)) {
} return $file->{$this->fileEditValidator}();
return null; }
}
throw new InvalidArgumentException("Invalid value for UploadField::fileEditValidator");
public function getAttributes() { }
return array_merge(
parent::getAttributes(), /**
array( * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm
'type' => 'file', * @example 'getCMSValidator'
'data-selectdialog-url' => $this->Link('select') *
) * @param Validator|string
); * @return Uploadfield Self reference
} */
public function setFileEditValidator($fileEditValidator)
public function extraClass() { {
if($this->isDisabled()) { $this->fileEditValidator = $fileEditValidator;
$this->addExtraClass('disabled'); return $this;
} }
if($this->isReadonly()) {
$this->addExtraClass('readonly'); /**
} *
* @param File|AssetContainer $file
return parent::extraClass(); * @return string URL to thumbnail
} */
protected function getThumbnailURLForFile(AssetContainer $file)
public function Field($properties = array()) { {
// Calculated config as per jquery.fileupload-ui.js if (!$file->exists()) {
$allowedMaxFileNumber = $this->getAllowedMaxFileNumber(); return null;
$config = array( }
'url' => $this->Link('upload'),
'urlSelectDialog' => $this->Link('select'), // Attempt to generate image at given size
'urlAttach' => $this->Link('attach'), $width = $this->getPreviewMaxWidth();
'urlFileExists' => $this->Link('fileexists'), $height = $this->getPreviewMaxHeight();
'acceptFileTypes' => '.+$', if ($file->hasMethod('ThumbnailURL')) {
// Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed return $file->ThumbnailURL($width, $height);
'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null, }
'replaceFile' => $this->getUpload()->getReplaceFile(), if ($file->hasMethod('Thumbnail')) {
); return $file->Thumbnail($width, $height)->getURL();
}
// Validation: File extensions if ($file->hasMethod('Fit')) {
if ($allowedExtensions = $this->getAllowedExtensions()) { return $file->Fit($width, $height)->getURL();
$config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; }
$config['errorMessages']['acceptFileTypes'] = _t(
'File.INVALIDEXTENSIONSHORT', // Check if unsized icon is available
'Extension is not allowed' if ($file->hasMethod('getIcon')) {
); return $file->getIcon();
} }
return null;
// Validation: File size }
if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
$config['maxFileSize'] = $allowedMaxFileSize; public function getAttributes()
$config['errorMessages']['maxFileSize'] = _t( {
'File.TOOLARGESHORT', return array_merge(
'File size exceeds {size}', parent::getAttributes(),
array('size' => File::format_size($config['maxFileSize'])) array(
); 'type' => 'file',
} 'data-selectdialog-url' => $this->Link('select')
)
// Validation: Number of files );
if ($allowedMaxFileNumber) { }
if($allowedMaxFileNumber > 1) {
$config['errorMessages']['maxNumberOfFiles'] = _t( public function extraClass()
'UploadField.MAXNUMBEROFFILESSHORT', {
'Can only upload {count} files', if ($this->isDisabled()) {
array('count' => $allowedMaxFileNumber) $this->addExtraClass('disabled');
); }
} else { if ($this->isReadonly()) {
$config['errorMessages']['maxNumberOfFiles'] = _t( $this->addExtraClass('readonly');
'UploadField.MAXNUMBEROFFILESONE', }
'Can only upload one file'
); return parent::extraClass();
} }
}
public function Field($properties = array())
// add overwrite warning error message to the config object sent to Javascript {
if ($this->getOverwriteWarning()) { // Calculated config as per jquery.fileupload-ui.js
$config['errorMessages']['overwriteWarning'] = $allowedMaxFileNumber = $this->getAllowedMaxFileNumber();
_t('UploadField.OVERWRITEWARNING', 'File with the same name already exists'); $config = array(
} 'url' => $this->Link('upload'),
'urlSelectDialog' => $this->Link('select'),
$mergedConfig = array_merge($config, $this->ufConfig); 'urlAttach' => $this->Link('attach'),
return parent::Field(array( 'urlFileExists' => $this->Link('fileexists'),
'configString' => Convert::raw2json($mergedConfig), 'acceptFileTypes' => '.+$',
'config' => new ArrayData($mergedConfig), // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed
'multiple' => $allowedMaxFileNumber !== 1 'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null,
)); 'replaceFile' => $this->getUpload()->getReplaceFile(),
} );
/** // Validation: File extensions
* Validation method for this field, called when the entire form is validated if ($allowedExtensions = $this->getAllowedExtensions()) {
* $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$';
* @param Validator $validator $config['errorMessages']['acceptFileTypes'] = _t(
* @return boolean 'File.INVALIDEXTENSIONSHORT',
*/ 'Extension is not allowed'
public function validate($validator) { );
$name = $this->getName(); }
$files = $this->getItems();
// Validation: File size
// If there are no files then quit if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) {
if($files->count() == 0) return true; $config['maxFileSize'] = $allowedMaxFileSize;
$config['errorMessages']['maxFileSize'] = _t(
// Check max number of files 'File.TOOLARGESHORT',
$maxFiles = $this->getAllowedMaxFileNumber(); 'File size exceeds {size}',
if($maxFiles && ($files->count() > $maxFiles)) { array('size' => File::format_size($config['maxFileSize']))
$validator->validationError( );
$name, }
_t(
'UploadField.MAXNUMBEROFFILES', // Validation: Number of files
'Max number of {count} file(s) exceeded.', if ($allowedMaxFileNumber) {
array('count' => $maxFiles) if ($allowedMaxFileNumber > 1) {
), $config['errorMessages']['maxNumberOfFiles'] = _t(
"validation" 'UploadField.MAXNUMBEROFFILESSHORT',
); 'Can only upload {count} files',
return false; array('count' => $allowedMaxFileNumber)
} );
} else {
// Revalidate each file against nested validator $config['errorMessages']['maxNumberOfFiles'] = _t(
$this->upload->clearErrors(); 'UploadField.MAXNUMBEROFFILESONE',
foreach($files as $file) { 'Can only upload one file'
// Generate $_FILES style file attribute array for upload validator );
$tmpFile = array( }
'name' => $file->Name, }
'type' => null, // Not used for type validation
'size' => $file->AbsoluteSize, // add overwrite warning error message to the config object sent to Javascript
'tmp_name' => null, // Should bypass is_uploaded_file check if ($this->getOverwriteWarning()) {
'error' => UPLOAD_ERR_OK, $config['errorMessages']['overwriteWarning'] =
); _t('UploadField.OVERWRITEWARNING', 'File with the same name already exists');
$this->upload->validate($tmpFile); }
}
$mergedConfig = array_merge($config, $this->ufConfig);
// Check all errors return parent::Field(array(
if($errors = $this->upload->getErrors()) { 'configString' => Convert::raw2json($mergedConfig),
foreach($errors as $error) { 'config' => new ArrayData($mergedConfig),
$validator->validationError($name, $error, "validation"); 'multiple' => $allowedMaxFileNumber !== 1
} ));
return false; }
}
/**
return true; * Validation method for this field, called when the entire form is validated
} *
* @param Validator $validator
/** * @return boolean
* @param HTTPRequest $request */
* @return UploadField_ItemHandler public function validate($validator)
*/ {
public function handleItem(HTTPRequest $request) { $name = $this->getName();
return $this->getItemHandler($request->param('ID')); $files = $this->getItems();
}
// If there are no files then quit
/** if ($files->count() == 0) {
* @param int $itemID return true;
* @return UploadField_ItemHandler }
*/
public function getItemHandler($itemID) { // Check max number of files
return UploadField_ItemHandler::create($this, $itemID); $maxFiles = $this->getAllowedMaxFileNumber();
} if ($maxFiles && ($files->count() > $maxFiles)) {
$validator->validationError(
/** $name,
* @param HTTPRequest $request _t(
* @return UploadField_SelectHandler 'UploadField.MAXNUMBEROFFILES',
*/ 'Max number of {count} file(s) exceeded.',
public function handleSelect(HTTPRequest $request) { array('count' => $maxFiles)
if(!$this->canAttachExisting()) { ),
return $this->httpError(403); "validation"
} );
return UploadField_SelectHandler::create($this, $this->getFolderName()); return false;
} }
/** // Revalidate each file against nested validator
* Safely encodes the File object with all standard fields required $this->upload->clearErrors();
* by the front end foreach ($files as $file) {
* // Generate $_FILES style file attribute array for upload validator
* @param File|AssetContainer $file Object which contains a file $tmpFile = array(
* @return array Array encoded list of file attributes 'name' => $file->Name,
*/ 'type' => null, // Not used for type validation
protected function encodeFileAttributes(AssetContainer $file) { 'size' => $file->AbsoluteSize,
// Collect all output data. 'tmp_name' => null, // Should bypass is_uploaded_file check
$customised = $this->customiseFile($file); 'error' => UPLOAD_ERR_OK,
return array( );
'id' => $file->ID, $this->upload->validate($tmpFile);
'name' => basename($file->getFilename()), }
'url' => $file->getURL(),
'thumbnail_url' => $customised->UploadFieldThumbnailURL, // Check all errors
'edit_url' => $customised->UploadFieldEditLink, if ($errors = $this->upload->getErrors()) {
'size' => $file->getAbsoluteSize(), foreach ($errors as $error) {
'type' => File::get_file_type($file->getFilename()), $validator->validationError($name, $error, "validation");
'buttons' => (string)$customised->UploadFieldFileButtons, }
'fieldname' => $this->getName() return false;
); }
}
return true;
/** }
* Action to handle upload of a single file
* /**
* @param HTTPRequest $request * @param HTTPRequest $request
* @return HTTPResponse * @return UploadField_ItemHandler
* @return HTTPResponse */
*/ public function handleItem(HTTPRequest $request)
public function upload(HTTPRequest $request) { {
if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { return $this->getItemHandler($request->param('ID'));
return $this->httpError(403); }
}
/**
// Protect against CSRF on destructive action * @param int $itemID
$token = $this->getForm()->getSecurityToken(); * @return UploadField_ItemHandler
if(!$token->checkRequest($request)) return $this->httpError(400); */
public function getItemHandler($itemID)
// Get form details {
$name = $this->getName(); return UploadField_ItemHandler::create($this, $itemID);
$postVars = $request->postVar($name); }
// Extract uploaded files from Form data /**
$uploadedFiles = $this->extractUploadedFileData($postVars); * @param HTTPRequest $request
$return = array(); * @return UploadField_SelectHandler
*/
// Save the temporary files into a File objects public function handleSelect(HTTPRequest $request)
// and save data/error on a per file basis {
foreach ($uploadedFiles as $tempFile) { if (!$this->canAttachExisting()) {
$file = $this->saveTemporaryFile($tempFile, $error); return $this->httpError(403);
if(empty($file)) { }
array_push($return, array('error' => $error)); return UploadField_SelectHandler::create($this, $this->getFolderName());
} else { }
array_push($return, $this->encodeFileAttributes($file));
} /**
$this->upload->clearErrors(); * Safely encodes the File object with all standard fields required
} * by the front end
*
// Format response with json * @param File|AssetContainer $file Object which contains a file
$response = new HTTPResponse(Convert::raw2json($return)); * @return array Array encoded list of file attributes
$response->addHeader('Content-Type', 'text/plain'); */
return $response; protected function encodeFileAttributes(AssetContainer $file)
} {
// Collect all output data.
/** $customised = $this->customiseFile($file);
* Retrieves details for files that this field wishes to attache to the return array(
* client-side form 'id' => $file->ID,
* 'name' => basename($file->getFilename()),
* @param HTTPRequest $request 'url' => $file->getURL(),
* @return HTTPResponse 'thumbnail_url' => $customised->UploadFieldThumbnailURL,
*/ 'edit_url' => $customised->UploadFieldEditLink,
public function attach(HTTPRequest $request) { 'size' => $file->getAbsoluteSize(),
if(!$request->isPOST()) return $this->httpError(403); 'type' => File::get_file_type($file->getFilename()),
if(!$this->canAttachExisting()) return $this->httpError(403); 'buttons' => (string)$customised->UploadFieldFileButtons,
'fieldname' => $this->getName()
// Retrieve file attributes required by front end );
$return = array(); }
$files = File::get()->byIDs($request->postVar('ids'));
foreach($files as $file) { /**
$return[] = $this->encodeFileAttributes($file); * Action to handle upload of a single file
} *
$response = new HTTPResponse(Convert::raw2json($return)); * @param HTTPRequest $request
$response->addHeader('Content-Type', 'application/json'); * @return HTTPResponse
return $response; * @return HTTPResponse
} */
public function upload(HTTPRequest $request)
/** {
* Check if file exists, both checking filtered filename and exact filename if ($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) {
* return $this->httpError(403);
* @param string $originalFile Filename }
* @return bool
*/ // Protect against CSRF on destructive action
protected function checkFileExists($originalFile) { $token = $this->getForm()->getSecurityToken();
if (!$token->checkRequest($request)) {
// Check both original and safely filtered filename return $this->httpError(400);
$nameFilter = FileNameFilter::create(); }
$filteredFile = $nameFilter->filter($originalFile);
// Get form details
// Resolve expected folder name $name = $this->getName();
$folderName = $this->getFolderName(); $postVars = $request->postVar($name);
$folder = Folder::find_or_make($folderName);
$parentPath = $folder ? $folder->getFilename() : ''; // Extract uploaded files from Form data
$uploadedFiles = $this->extractUploadedFileData($postVars);
// check if either file exists $return = array();
return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
} // Save the temporary files into a File objects
// and save data/error on a per file basis
/** foreach ($uploadedFiles as $tempFile) {
* Determines if a specified file exists $file = $this->saveTemporaryFile($tempFile, $error);
* if (empty($file)) {
* @param HTTPRequest $request array_push($return, array('error' => $error));
* @return HTTPResponse } else {
*/ array_push($return, $this->encodeFileAttributes($file));
public function fileexists(HTTPRequest $request) { }
// Assert that requested filename doesn't attempt to escape the directory $this->upload->clearErrors();
$originalFile = $request->requestVar('filename'); }
if($originalFile !== basename($originalFile)) {
$return = array( // Format response with json
'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload') $response = new HTTPResponse(Convert::raw2json($return));
); $response->addHeader('Content-Type', 'text/plain');
} else { return $response;
$return = array( }
'exists' => $this->checkFileExists($originalFile)
); /**
} * Retrieves details for files that this field wishes to attache to the
* client-side form
// Encode and present response *
$response = new HTTPResponse(Convert::raw2json($return)); * @param HTTPRequest $request
$response->addHeader('Content-Type', 'application/json'); * @return HTTPResponse
if (!empty($return['error'])) $response->setStatusCode(400); */
return $response; public function attach(HTTPRequest $request)
} {
if (!$request->isPOST()) {
public function performReadonlyTransformation() { return $this->httpError(403);
$clone = clone $this; }
$clone->addExtraClass('readonly'); if (!$this->canAttachExisting()) {
$clone->setReadonly(true); return $this->httpError(403);
return $clone; }
}
// Retrieve file attributes required by front end
$return = array();
$files = File::get()->byIDs($request->postVar('ids'));
foreach ($files as $file) {
$return[] = $this->encodeFileAttributes($file);
}
$response = new HTTPResponse(Convert::raw2json($return));
$response->addHeader('Content-Type', 'application/json');
return $response;
}
/**
* Check if file exists, both checking filtered filename and exact filename
*
* @param string $originalFile Filename
* @return bool
*/
protected function checkFileExists($originalFile)
{
// Check both original and safely filtered filename
$nameFilter = FileNameFilter::create();
$filteredFile = $nameFilter->filter($originalFile);
// Resolve expected folder name
$folderName = $this->getFolderName();
$folder = Folder::find_or_make($folderName);
$parentPath = $folder ? $folder->getFilename() : '';
// check if either file exists
return File::find($parentPath.$originalFile) || File::find($parentPath.$filteredFile);
}
/**
* Determines if a specified file exists
*
* @param HTTPRequest $request
* @return HTTPResponse
*/
public function fileexists(HTTPRequest $request)
{
// Assert that requested filename doesn't attempt to escape the directory
$originalFile = $request->requestVar('filename');
if ($originalFile !== basename($originalFile)) {
$return = array(
'error' => _t('File.NOVALIDUPLOAD', 'File is not a valid upload')
);
} else {
$return = array(
'exists' => $this->checkFileExists($originalFile)
);
}
// Encode and present response
$response = new HTTPResponse(Convert::raw2json($return));
$response->addHeader('Content-Type', 'application/json');
if (!empty($return['error'])) {
$response->setStatusCode(400);
}
return $response;
}
public function performReadonlyTransformation()
{
$clone = clone $this;
$clone->addExtraClass('readonly');
$clone->setReadonly(true);
return $clone;
}
} }

View File

@ -16,140 +16,150 @@ use SilverStripe\Assets\Upload_Validator;
*/ */
trait UploadReceiver trait UploadReceiver
{ {
/** /**
* Upload object (needed for validation * Upload object (needed for validation
* and actually moving the temporary file * and actually moving the temporary file
* created by PHP). * created by PHP).
* *
* @var Upload * @var Upload
*/ */
protected $upload; protected $upload;
/** /**
* Partial filesystem path relative to /assets directory. * Partial filesystem path relative to /assets directory.
* Defaults to Upload::$uploads_folder. * Defaults to Upload::$uploads_folder.
* *
* @var string * @var string
*/ */
protected $folderName = false; protected $folderName = false;
/** /**
* Bootstrap Uploadable field * Bootstrap Uploadable field
*/ */
protected function constructUploadReceiver() { protected function constructUploadReceiver()
// Set Upload instance {
$this->setUpload(Upload::create()); // Set Upload instance
$this->setUpload(Upload::create());
// filter out '' since this would be a regex problem on JS end // filter out '' since this would be a regex problem on JS end
$this->getValidator()->setAllowedExtensions( $this->getValidator()->setAllowedExtensions(
array_filter(File::config()->allowed_extensions) array_filter(File::config()->allowed_extensions)
); );
// get the lower max size // get the lower max size
$maxUpload = File::ini2bytes(ini_get('upload_max_filesize')); $maxUpload = File::ini2bytes(ini_get('upload_max_filesize'));
$maxPost = File::ini2bytes(ini_get('post_max_size')); $maxPost = File::ini2bytes(ini_get('post_max_size'));
$this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost)); $this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost));
} }
/** /**
* Retrieves the Upload handler * Retrieves the Upload handler
* *
* @return Upload * @return Upload
*/ */
public function getUpload() { public function getUpload()
return $this->upload; {
} return $this->upload;
}
/** /**
* Sets the upload handler * Sets the upload handler
* *
* @param Upload $upload * @param Upload $upload
* @return $this Self reference * @return $this Self reference
*/ */
public function setUpload(Upload $upload) { public function setUpload(Upload $upload)
$this->upload = $upload; {
return $this; $this->upload = $upload;
} return $this;
}
/** /**
* Limit allowed file extensions. Empty by default, allowing all extensions. * Limit allowed file extensions. Empty by default, allowing all extensions.
* To allow files without an extension, use an empty string. * To allow files without an extension, use an empty string.
* See {@link File::$allowed_extensions} to get a good standard set of * See {@link File::$allowed_extensions} to get a good standard set of
* extensions that are typically not harmful in a webserver context. * extensions that are typically not harmful in a webserver context.
* See {@link setAllowedMaxFileSize()} to limit file size by extension. * See {@link setAllowedMaxFileSize()} to limit file size by extension.
* *
* @param array $rules List of extensions * @param array $rules List of extensions
* @return $this * @return $this
*/ */
public function setAllowedExtensions($rules) { public function setAllowedExtensions($rules)
$this->getValidator()->setAllowedExtensions($rules); {
return $this; $this->getValidator()->setAllowedExtensions($rules);
} return $this;
}
/** /**
* Limit allowed file extensions by specifying categories of file types. * Limit allowed file extensions by specifying categories of file types.
* These may be 'image', 'image/supported', 'audio', 'video', 'archive', 'flash', or 'document' * These may be 'image', 'image/supported', 'audio', 'video', 'archive', 'flash', or 'document'
* See {@link File::$allowed_extensions} for details of allowed extensions * See {@link File::$allowed_extensions} for details of allowed extensions
* for each of these categories * for each of these categories
* *
* @param string $category Category name * @param string $category Category name
* @param string,... $categories Additional category names * @param string,... $categories Additional category names
* @return $this * @return $this
*/ */
public function setAllowedFileCategories($category) { public function setAllowedFileCategories($category)
$extensions = File::get_category_extensions(func_get_args()); {
return $this->setAllowedExtensions($extensions); $extensions = File::get_category_extensions(func_get_args());
} return $this->setAllowedExtensions($extensions);
}
/** /**
* Returns list of extensions allowed by this field, or an empty array * Returns list of extensions allowed by this field, or an empty array
* if there is no restriction * if there is no restriction
* *
* @return array * @return array
*/ */
public function getAllowedExtensions() { public function getAllowedExtensions()
return $this->getValidator()->getAllowedExtensions(); {
} return $this->getValidator()->getAllowedExtensions();
}
/** /**
* Get custom validator for this field * Get custom validator for this field
* *
* @return Upload_Validator * @return Upload_Validator
*/ */
public function getValidator() { public function getValidator()
return $this->getUpload()->getValidator(); {
} return $this->getUpload()->getValidator();
}
/** /**
* Set custom validator for this field * Set custom validator for this field
* *
* @param Upload_Validator $validator * @param Upload_Validator $validator
* @return $this Self reference * @return $this Self reference
*/ */
public function setValidator(Upload_Validator $validator) { public function setValidator(Upload_Validator $validator)
$this->getUpload()->setValidator($validator); {
return $this; $this->getUpload()->setValidator($validator);
} return $this;
}
/** /**
* Sets the upload folder name * Sets the upload folder name
* *
* @param string $folderName * @param string $folderName
* @return $this Self reference * @return $this Self reference
*/ */
public function setFolderName($folderName) { public function setFolderName($folderName)
$this->folderName = $folderName; {
return $this; $this->folderName = $folderName;
} return $this;
}
/** /**
* Gets the upload folder name * Gets the upload folder name
* *
* @return string * @return string
*/ */
public function getFolderName() { public function getFolderName()
return ($this->folderName !== false) {
? $this->folderName return ($this->folderName !== false)
: Upload::config()->uploads_folder; ? $this->folderName
} : Upload::config()->uploads_folder;
}
} }