From 7cba50e3a544780badc4e1eda6ed3e2cf8b49417 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 4 Nov 2016 15:40:34 +1300 Subject: [PATCH] API Refactor UploadField, FileField and AssetField into traits Uploadable and FileUploadable --- src/Assets/Upload.php | 634 ++++----- src/Assets/Upload_Validator.php | 2 + src/Forms/AssetField.php | 1246 ++++++++-------- src/Forms/FileField.php | 369 ++--- src/Forms/FileUploadable.php | 399 ++++++ src/Forms/UploadField.php | 2356 +++++++++++++------------------ src/Forms/Uploadable.php | 153 ++ 7 files changed, 2601 insertions(+), 2558 deletions(-) create mode 100644 src/Forms/FileUploadable.php create mode 100644 src/Forms/Uploadable.php diff --git a/src/Assets/Upload.php b/src/Assets/Upload.php index 444a344a4..aed1fe12f 100644 --- a/src/Assets/Upload.php +++ b/src/Assets/Upload.php @@ -33,405 +33,405 @@ use Exception; class Upload extends Controller { - private static $allowed_actions = array( - 'index', - 'load' - ); + private static $allowed_actions = array( + 'index', + 'load' + ); - /** - * A dataobject (typically {@see File}) which implements {@see AssetContainer} - * - * @var AssetContainer - */ - protected $file; + /** + * A dataobject (typically {@see File}) which implements {@see AssetContainer} + * + * @var AssetContainer + */ + protected $file; - /** - * Validator for this upload field - * - * @var Upload_Validator - */ - protected $validator; + /** + * Validator for this upload field + * + * @var Upload_Validator + */ + protected $validator; - /** - * Information about the temporary file produced - * by the PHP-runtime. - * - * @var array - */ - protected $tmpFile; + /** + * Information about the temporary file produced + * by the PHP-runtime. + * + * @var array + */ + protected $tmpFile; - /** - * Replace an existing file rather than renaming the new one. - * - * @var boolean - */ - protected $replaceFile = false; + /** + * Replace an existing file rather than renaming the new one. + * + * @var boolean + */ + protected $replaceFile = false; - /** - * Processing errors that can be evaluated, - * e.g. by Form-validation. - * - * @var array - */ - protected $errors = array(); + /** + * Processing errors that can be evaluated, + * e.g. by Form-validation. + * + * @var array + */ + protected $errors = array(); - /** - * Default visibility to assign uploaded files - * - * @var string - */ - protected $defaultVisibility = AssetStore::VISIBILITY_PROTECTED; + /** + * Default visibility to assign uploaded files + * + * @var string + */ + protected $defaultVisibility = AssetStore::VISIBILITY_PROTECTED; - /** - * A foldername relative to /assets, - * where all uploaded files are stored by default. - * - * @config - * @var string - */ - private static $uploads_folder = "Uploads"; + /** + * A foldername relative to /assets, + * where all uploaded files are stored by default. + * + * @config + * @var string + */ + private static $uploads_folder = "Uploads"; - /** - * A prefix for the version number added to an uploaded file - * when a file with the same name already exists. - * Example using no prefix: IMG001.jpg becomes IMG2.jpg - * Example using '-v' prefix: IMG001.jpg becomes IMG001-v2.jpg - * - * @config - * @var string - */ - private static $version_prefix = '-v'; + /** + * A prefix for the version number added to an uploaded file + * when a file with the same name already exists. + * Example using no prefix: IMG001.jpg becomes IMG2.jpg + * Example using '-v' prefix: IMG001.jpg becomes IMG001-v2.jpg + * + * @config + * @var string + */ + private static $version_prefix = '-v'; public function __construct() { - parent::__construct(); - $this->validator = Injector::inst()->create('SilverStripe\\Assets\\Upload_Validator'); - $this->replaceFile = self::config()->replaceFile; - } + parent::__construct(); + $this->validator = Upload_Validator::create(); + $this->replaceFile = self::config()->replaceFile; + } public function index() { - return $this->httpError(404); // no-op - } + return $this->httpError(404); // no-op + } - /** - * Get current validator - * - * @return Upload_Validator $validator - */ + /** + * Get current validator + * + * @return Upload_Validator $validator + */ public function getValidator() { - return $this->validator; - } + return $this->validator; + } - /** - * Set a different instance than {@link Upload_Validator} - * for this upload session. - * - * @param object $validator - */ + /** + * Set a different instance than {@link Upload_Validator} + * for this upload session. + * + * @param object $validator + */ public function setValidator($validator) { - $this->validator = $validator; - } + $this->validator = $validator; + } - /** - * Get an asset renamer for the given filename. - * - * @param string $filename Path name - * @return AssetNameGenerator - */ + /** + * Get an asset renamer for the given filename. + * + * @param string $filename Path name + * @return AssetNameGenerator + */ protected function getNameGenerator($filename) { - return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename)); - } + return Injector::inst()->createWithArgs('AssetNameGenerator', array($filename)); + } - /** - * - * @return AssetStore - */ + /** + * + * @return AssetStore + */ 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 - * - * @param array $tmpFile Indexed array that PHP generated for every file it uploads. - * @param string|bool $folderPath Folder path relative to /assets - * @return array|false Either the tuple array, or false if the file could not be saved - */ + /** + * Save an file passed from a form post into the AssetStore directly + * + * @param array $tmpFile Indexed array that PHP generated for every file it uploads. + * @param string|bool $folderPath Folder path relative to /assets + * @return array|false Either the tuple array, or false if the file could not be saved + */ public function load($tmpFile, $folderPath = false) { - // Validate filename - $filename = $this->getValidFilename($tmpFile, $folderPath); - if (!$filename) { - return false; - } + // Validate filename + $filename = $this->getValidFilename($tmpFile, $folderPath); + if(!$filename) { + return false; + } - // Save file into backend - $result = $this->storeTempFile($tmpFile, $filename, $this->getAssetStore()); + // Save file into backend + $result = $this->storeTempFile($tmpFile, $filename, $this->getAssetStore()); - //to allow extensions to e.g. create a version after an upload - $this->extend('onAfterLoad', $result, $tmpFile); - return $result; - } + //to allow extensions to e.g. create a version after an upload + $this->extend('onAfterLoad', $result, $tmpFile); + return $result; + } - /** - * Save an file passed from a form post into this object. - * File names are filtered through {@link FileNameFilter}, see class documentation - * on how to influence this behaviour. - * - * @param array $tmpFile - * @param AssetContainer $file - * @param string|bool $folderPath - * @return bool True if the file was successfully saved into this record - * @throws Exception - */ + /** + * Save an file passed from a form post into this object. + * File names are filtered through {@link FileNameFilter}, see class documentation + * on how to influence this behaviour. + * + * @param array $tmpFile + * @param AssetContainer $file + * @param string|bool $folderPath + * @return bool True if the file was successfully saved into this record + * @throws Exception + */ public function loadIntoFile($tmpFile, $file = null, $folderPath = false) { - $this->file = $file; + $this->file = $file; - // Validate filename - $filename = $this->getValidFilename($tmpFile, $folderPath); - if (!$filename) { - return false; - } - $filename = $this->resolveExistingFile($filename); + // Validate filename + $filename = $this->getValidFilename($tmpFile, $folderPath); + if(!$filename) { + return false; + } + $filename = $this->resolveExistingFile($filename); - // Save changes to underlying record (if it's a DataObject) - $this->storeTempFile($tmpFile, $filename, $this->file); - if ($this->file instanceof DataObject) { - $this->file->write(); - } + // Save changes to underlying record (if it's a DataObject) + $this->storeTempFile($tmpFile, $filename, $this->file); + if($this->file instanceof DataObject) { + $this->file->write(); + } - //to allow extensions to e.g. create a version after an upload - $this->file->extend('onAfterUpload'); - $this->extend('onAfterLoadIntoFile', $this->file); - return true; - } + //to allow extensions to e.g. create a version after an upload + $this->file->extend('onAfterUpload'); + $this->extend('onAfterLoadIntoFile', $this->file); + return true; + } - /** - * Assign this temporary file into the given destination - * - * @param array $tmpFile - * @param string $filename - * @param AssetContainer|AssetStore $container - * @return array - */ + /** + * Assign this temporary file into the given destination + * + * @param array $tmpFile + * @param string $filename + * @param AssetContainer|AssetStore $container + * @return array + */ protected function storeTempFile($tmpFile, $filename, $container) { - // Save file into backend - $conflictResolution = $this->replaceFile - ? AssetStore::CONFLICT_OVERWRITE - : AssetStore::CONFLICT_RENAME; - $config = array( - 'conflict' => $conflictResolution, - 'visibility' => $this->getDefaultVisibility() - ); - return $container->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $config); - } + // Save file into backend + $conflictResolution = $this->replaceFile + ? AssetStore::CONFLICT_OVERWRITE + : AssetStore::CONFLICT_RENAME; + $config = array( + 'conflict' => $conflictResolution, + 'visibility' => $this->getDefaultVisibility() + ); + return $container->setFromLocalFile($tmpFile['tmp_name'], $filename, null, null, $config); + } - /** - * Given a temporary file and upload path, validate the file and determine the - * value of the 'Filename' tuple that should be used to store this asset. - * - * @param array $tmpFile - * @param string $folderPath - * @return string|false Value of filename tuple, or false if invalid - */ + /** + * Given a temporary file and upload path, validate the file and determine the + * value of the 'Filename' tuple that should be used to store this asset. + * + * @param array $tmpFile + * @param string $folderPath + * @return string|false Value of filename tuple, or false if invalid + */ protected function getValidFilename($tmpFile, $folderPath = null) { - if (!is_array($tmpFile)) { - throw new InvalidArgumentException( - "Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype" - ); - } + if(!is_array($tmpFile)) { + throw new InvalidArgumentException( + "Upload::load() Not passed an array. Most likely, the form hasn't got the right enctype" + ); + } - // Validate - $this->clearErrors(); - $valid = $this->validate($tmpFile); - if (!$valid) { - return false; - } + // Validate + $this->clearErrors(); + $valid = $this->validate($tmpFile); + if(!$valid) { + return false; + } - // Clean filename - if (!$folderPath) { - $folderPath = $this->config()->uploads_folder; - } - $nameFilter = FileNameFilter::create(); - $file = $nameFilter->filter($tmpFile['name']); - $filename = basename($file); - if ($folderPath) { - $filename = File::join_paths($folderPath, $filename); - } - return $filename; - } + // Clean filename + if(!$folderPath) { + $folderPath = $this->config()->uploads_folder; + } + $nameFilter = FileNameFilter::create(); + $file = $nameFilter->filter($tmpFile['name']); + $filename = basename($file); + if($folderPath) { + $filename = File::join_paths($folderPath, $filename); + } + return $filename; + } - /** - * Given a file and filename, ensure that file renaming / replacing rules are satisfied - * - * If replacing, this method may replace $this->file with an existing record to overwrite. - * If renaming, a new value for $filename may be returned - * - * @param string $filename - * @return string $filename A filename safe to write to - * @throws Exception - */ + /** + * Given a file and filename, ensure that file renaming / replacing rules are satisfied + * + * If replacing, this method may replace $this->file with an existing record to overwrite. + * If renaming, a new value for $filename may be returned + * + * @param string $filename + * @return string $filename A filename safe to write to + * @throws Exception + */ protected function resolveExistingFile($filename) { - // Create a new file record (or try to retrieve an existing one) - if (!$this->file) { - $fileClass = File::get_class_for_file_extension( - File::get_file_extension($filename) - ); - $this->file = Object::create($fileClass); - } + // Create a new file record (or try to retrieve an existing one) + if(!$this->file) { + $fileClass = File::get_class_for_file_extension( + File::get_file_extension($filename) + ); + $this->file = Object::create($fileClass); + } - // Skip this step if not writing File dataobjects - if (! ($this->file instanceof File)) { - return $filename; - } + // Skip this step if not writing File dataobjects + if(! ($this->file instanceof File) ) { + return $filename; + } - // Check there is if existing file - $existing = File::find($filename); + // Check there is if existing file + $existing = File::find($filename); - // If replacing (or no file exists) confirm this filename is safe - if ($this->replaceFile || !$existing) { - // If replacing files, make sure to update the OwnerID - if (!$this->file->ID && $this->replaceFile && $existing) { - $this->file = $existing; - $this->file->OwnerID = Member::currentUserID(); - } - // Filename won't change if replacing - return $filename; - } + // If replacing (or no file exists) confirm this filename is safe + if($this->replaceFile || !$existing) { + // If replacing files, make sure to update the OwnerID + if(!$this->file->ID && $this->replaceFile && $existing) { + $this->file = $existing; + $this->file->OwnerID = Member::currentUserID(); + } + // Filename won't change if replacing + return $filename; + } - // if filename already exists, version the filename (e.g. test.gif to test-v2.gif, test-v2.gif to test-v3.gif) - $renamer = $this->getNameGenerator($filename); - foreach ($renamer as $newName) { - if (!File::find($newName)) { - return $newName; - } - } + // if filename already exists, version the filename (e.g. test.gif to test-v2.gif, test-v2.gif to test-v3.gif) + $renamer = $this->getNameGenerator($filename); + foreach($renamer as $newName) { + if(!File::find($newName)) { + return $newName; + } + } - // Fail - $tries = $renamer->getMaxTries(); - throw new Exception("Could not rename {$filename} with {$tries} tries"); - } + // Fail + $tries = $renamer->getMaxTries(); + throw new Exception("Could not rename {$filename} with {$tries} tries"); + } - /** - * @param bool $replace - */ + /** + * @param bool $replace + */ public function setReplaceFile($replace) { - $this->replaceFile = $replace; - } + $this->replaceFile = $replace; + } - /** - * @return bool - */ + /** + * @return bool + */ public function getReplaceFile() { - return $this->replaceFile; - } + return $this->replaceFile; + } - /** - * Container for all validation on the file - * (e.g. size and extension restrictions). - * Is NOT connected to the {Validator} classes, - * please have a look at {FileField->validate()} - * for an example implementation of external validation. - * - * @param array $tmpFile - * @return boolean - */ + /** + * Container for all validation on the file + * (e.g. size and extension restrictions). + * Is NOT connected to the {Validator} classes, + * please have a look at {FileField->validate()} + * for an example implementation of external validation. + * + * @param array $tmpFile + * @return boolean + */ public function validate($tmpFile) { - $validator = $this->validator; - $validator->setTmpFile($tmpFile); - $isValid = $validator->validate(); - if ($validator->getErrors()) { - $this->errors = array_merge($this->errors, $validator->getErrors()); - } - return $isValid; - } + $validator = $this->validator; + $validator->setTmpFile($tmpFile); + $isValid = $validator->validate(); + if($validator->getErrors()) { + $this->errors = array_merge($this->errors, $validator->getErrors()); + } + return $isValid; + } - /** - * Get file-object, either generated from {load()}, - * or manually set. - * - * @return AssetContainer - */ + /** + * Get file-object, either generated from {load()}, + * or manually set. + * + * @return AssetContainer + */ public function getFile() { - return $this->file; - } + return $this->file; + } - /** - * Set a file-object (similiar to {loadIntoFile()}) - * - * @param AssetContainer $file - */ + /** + * Set a file-object (similiar to {loadIntoFile()}) + * + * @param AssetContainer $file + */ public function setFile(AssetContainer $file) { - $this->file = $file; - } + $this->file = $file; + } - /** - * Clear out all errors (mostly set by {loadUploaded()}) - * including the validator's errors - */ + /** + * Clear out all errors (mostly set by {loadUploaded()}) + * including the validator's errors + */ public function clearErrors() { - $this->errors = array(); - $this->validator->clearErrors(); - } + $this->errors = array(); + $this->validator->clearErrors(); + } - /** - * Determines wether previous operations caused an error. - * - * @return boolean - */ + /** + * Determines wether previous operations caused an error. + * + * @return boolean + */ public function isError() { - return (count($this->errors)); - } + return (count($this->errors)); + } - /** - * Return all errors that occurred while processing so far - * (mostly set by {loadUploaded()}) - * - * @return array - */ + /** + * Return all errors that occurred while processing so far + * (mostly set by {loadUploaded()}) + * + * @return array + */ public function getErrors() { - return $this->errors; - } + return $this->errors; + } - /** - * Get default visibility for uploaded files. {@see AssetStore} - * One of the values of AssetStore::VISIBILITY_* constants - * - * @return string - */ + /** + * Get default visibility for uploaded files. {@see AssetStore} + * One of the values of AssetStore::VISIBILITY_* constants + * + * @return string + */ public function getDefaultVisibility() { - return $this->defaultVisibility; - } + return $this->defaultVisibility; + } - /** - * Assign default visibility for uploaded files. {@see AssetStore} - * One of the values of AssetStore::VISIBILITY_* constants - * - * @param string $visibility - * @return $this - */ + /** + * Assign default visibility for uploaded files. {@see AssetStore} + * One of the values of AssetStore::VISIBILITY_* constants + * + * @param string $visibility + * @return $this + */ public function setDefaultVisibility($visibility) { - $this->defaultVisibility = $visibility; - return $this; - } + $this->defaultVisibility = $visibility; + return $this; + } } diff --git a/src/Assets/Upload_Validator.php b/src/Assets/Upload_Validator.php index 8d8038a4d..4cb13be53 100644 --- a/src/Assets/Upload_Validator.php +++ b/src/Assets/Upload_Validator.php @@ -3,10 +3,12 @@ namespace SilverStripe\Assets; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Injector\Injectable; use SilverStripe\Dev\SapphireTest; class Upload_Validator { + use Injectable; /** * Contains a list of the max file sizes shared by diff --git a/src/Forms/AssetField.php b/src/Forms/AssetField.php index e7347a82f..f58891b01 100644 --- a/src/Forms/AssetField.php +++ b/src/Forms/AssetField.php @@ -14,7 +14,6 @@ use SilverStripe\Core\Convert; use SilverStripe\Core\Injector\Injector; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\HTTPResponse; -use SilverStripe\View\Requirements; use Exception; /** @@ -27,784 +26,793 @@ use Exception; * - Files can't be edited once uploaded. * - Attached files can only be removed, not deleted. */ -class AssetField extends FileField +class AssetField extends FormField { + use Uploadable; - /** - * @var array - */ - private static $allowed_actions = array( - 'upload' - ); + /** + * @var array + */ + private static $allowed_actions = array( + 'upload' + ); - /** - * @var array - */ - private static $url_handlers = array( - '$Action!' => '$Action', - ); + /** + * @var array + */ + private static $url_handlers = array( + '$Action!' => '$Action', + ); - private static $casting = array( - 'Value' => 'DBFile', - 'UploadFieldThumbnailURL' => 'Varchar' - ); + private static $casting = array( + 'Value' => 'DBFile', + 'UploadFieldThumbnailURL' => 'Varchar' + ); - /** - * Template to use for the file button widget - * - * @var string - */ - protected $templateFileButtons = null; + /** + * Template to use for the file button widget + * + * @var string + */ + 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. - * - * @var DataObject - */ - protected $record; + /** + * Parent data record. Will be infered from parent form or controller if blank. The destination + * DBFile should be a property of the name $name on this object. + * + * @var DataObject + */ + protected $record; - /** - * Config for this field used in the front-end javascript - * (will be merged into the config of the javascript file upload plugin). - * - * @var array - */ - protected $ufConfig = array(); + /** + * Config for this field used in the front-end javascript + * (will be merged into the config of the javascript file upload plugin). + * + * @var array + */ + protected $ufConfig = array(); - /** - * Front end config defaults - * - * @config - * @var array - */ - private static $defaultConfig = array( - /** - * Automatically upload the file once selected - * - * @var boolean - */ - 'autoUpload' => true, + /** + * Front end config defaults + * + * @config + * @var array + */ + private static $defaultConfig = array( + /** + * Automatically upload the file once selected + * + * @var boolean + */ + 'autoUpload' => true, - /** - * Can the user upload new files. - * String values are interpreted as permission codes. - * - * @var boolean|string - */ - 'canUpload' => true, + /** + * Can the user upload new files. + * String values are interpreted as permission codes. + * + * @var boolean|string + */ + 'canUpload' => true, - /** - * Shows the target folder for new uploads in the field UI. - * Disable to keep the internal filesystem structure hidden from users. - * - * @var boolean|string - */ - 'canPreviewFolder' => true, + /** + * Shows the target folder for new uploads in the field UI. + * Disable to keep the internal filesystem structure hidden from users. + * + * @var boolean|string + */ + 'canPreviewFolder' => true, - /** - * Indicate a change event to the containing form if an upload - * or file edit/delete was performed. - * - * @var boolean - */ - 'changeDetection' => true, + /** + * Indicate a change event to the containing form if an upload + * or file edit/delete was performed. + * + * @var boolean + */ + 'changeDetection' => true, - /** - * Maximum width of the preview thumbnail - * - * @var integer - */ - 'previewMaxWidth' => 80, + /** + * Maximum width of the preview thumbnail + * + * @var integer + */ + 'previewMaxWidth' => 80, - /** - * Maximum height of the preview thumbnail - * - * @var integer - */ - 'previewMaxHeight' => 60, + /** + * Maximum height of the preview thumbnail + * + * @var integer + */ + 'previewMaxHeight' => 60, - /** - * javascript template used to display uploading files - * - * @see javascript/UploadField_uploadtemplate.js - * @var string - */ - 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', + /** + * javascript template used to display uploading files + * + * @see javascript/UploadField_uploadtemplate.js + * @var string + */ + 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', - /** - * javascript template used to display already uploaded files - * - * @see javascript/UploadField_downloadtemplate.js - * @var string - */ - 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate' - ); + /** + * javascript template used to display already uploaded files + * + * @see javascript/UploadField_downloadtemplate.js + * @var string + */ + 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate' + ); - /** - * Folder to display in "Select files" list. - * Defaults to listing all files regardless of folder. - * The folder path should be relative to the webroot. - * See {@link FileField->folderName} to set the upload target instead. - * - * @var string - * @example admin/folder/subfolder - */ - protected $displayFolderName; + /** + * Folder to display in "Select files" list. + * Defaults to listing all files regardless of folder. + * The folder path should be relative to the webroot. + * See {@link FileField->folderName} to set the upload target instead. + * + * @var string + * @example admin/folder/subfolder + */ + protected $displayFolderName; - /** - * Construct a new UploadField instance - * - * @param string $name The internal field name, passed to forms. - * @param string $title The field label. - */ + /** + * Construct a new UploadField instance + * + * @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 - $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 = array_merge($this->ufConfig, self::config()->defaultConfig); + $this->ufConfig = array_merge($this->ufConfig, self::config()->defaultConfig); - parent::__construct($name, $title); + $this->constructUploadable(); + parent::__construct($name, $title); - // AssetField always uses rename replacement method - $this->getUpload()->setReplaceFile(false); + // AssetField always uses rename replacement method + $this->getUpload()->setReplaceFile(false); - // filter out '' since this would be a regex problem on JS end - $this->getValidator()->setAllowedExtensions( - array_filter(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')); - $this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost)); - } + // get the lower max size + $maxUpload = File::ini2bytes(ini_get('upload_max_filesize')); + $maxPost = File::ini2bytes(ini_get('post_max_size')); + $this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost)); + } - /** - * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) - * - * @param string - * @return $this - */ + /** + * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) + * + * @param string + * @return $this + */ public function setTemplateFileButtons($template) { - $this->templateFileButtons = $template; - return $this; - } + $this->templateFileButtons = $template; + return $this; + } - /** - * @return string - */ + /** + * @return string + */ public function getTemplateFileButtons() { - return $this->_templates($this->templateFileButtons, '_FileButtons'); - } + return $this->_templates($this->templateFileButtons, '_FileButtons'); + } - /** - * Determine if the target folder for new uploads in is visible the field UI. - * - * @return boolean - */ + /** + * Determine if the target folder for new uploads in is visible the field UI. + * + * @return boolean + */ public function canPreviewFolder() { - if (!$this->isActive()) { - return false; - } - $can = $this->getConfig('canPreviewFolder'); - if (is_bool($can)) { - return $can; - } - return Permission::check($can); - } + if(!$this->isActive()) { + return false; + } + $can = $this->getConfig('canPreviewFolder'); + if(is_bool($can)) { + return $can; + } + return Permission::check($can); + } - /** - * Determine if the target folder for new uploads in is visible the field UI. - * Disable to keep the internal filesystem structure hidden from users. - * - * @param boolean|string $canPreviewFolder Either a boolean flag, or a - * required permission code - * @return $this Self reference - */ + /** + * Determine if the target folder for new uploads in is visible the field UI. + * Disable to keep the internal filesystem structure hidden from users. + * + * @param boolean|string $canPreviewFolder Either a boolean flag, or a + * required permission code + * @return $this Self reference + */ public function setCanPreviewFolder($canPreviewFolder) { - return $this->setConfig('canPreviewFolder', $canPreviewFolder); - } + return $this->setConfig('canPreviewFolder', $canPreviewFolder); + } - /** - * @param string - * @return $this - */ + /** + * @param string + * @return $this + */ public function setDisplayFolderName($name) { - $this->displayFolderName = $name; - return $this; - } + $this->displayFolderName = $name; + return $this; + } - /** - * @return string - */ + /** + * @return string + */ public function getDisplayFolderName() { - return $this->displayFolderName; - } + return $this->displayFolderName; + } - /** - * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) - * - * @param DataObject $record - * @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 + * @return $this + */ 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(). - * - * @return 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(). + * + * @return DataObject + */ public function getRecord() { - if (!$this->record - && $this->form - && ($record = $this->form->getRecord()) - && $record instanceof DataObject - ) { - $this->record = $record; - } - return $this->record; - } + if (!$this->record + && $this->form + && ($record = $this->form->getRecord()) + && $record instanceof DataObject + ) { + $this->record = $record; + } + return $this->record; + } public function setValue($value, $record = null) { - // Extract value from underlying record - if (empty($value) && $this->getName() && $record instanceof DataObject) { - $name = $this->getName(); - $value = $record->$name; - } + // Extract value from underlying record + if(empty($value) && $this->getName() && $record instanceof DataObject) { + $name = $this->getName(); + $value = $record->$name; + } - // Convert asset container to tuple value - if ($value instanceof AssetContainer) { - if ($value->exists()) { - $value = array( - 'Filename' => $value->getFilename(), - 'Hash' => $value->getHash(), - 'Variant' => $value->getVariant() - ); - } else { - $value = null; - } - } + // Convert asset container to tuple value + if($value instanceof AssetContainer) { + if($value->exists()) { + $value = array( + 'Filename' => $value->getFilename(), + 'Hash' => $value->getHash(), + 'Variant' => $value->getVariant() + ); + } else { + $value = null; + } + } - // If javascript is disabled, direct file upload (non-html5 style) can - // trigger a single or multiple file submission. Note that this may be - // included in addition to re-submitted File IDs as above, so these - // should be added to the list instead of operated on independently. - if ($uploadedFile = $this->extractUploadedFileData($value)) { - $value = $this->saveTemporaryFile($uploadedFile, $error); - if (!$value) { - throw new ValidationException($error); - } - } + // If javascript is disabled, direct file upload (non-html5 style) can + // trigger a single or multiple file submission. Note that this may be + // included in addition to re-submitted File IDs as above, so these + // should be added to the list instead of operated on independently. + if($uploadedFile = $this->extractUploadedFileData($value)) { + $value = $this->saveTemporaryFile($uploadedFile, $error); + if(!$value) { + throw new ValidationException($error); + } + } - // Set value using parent - return parent::setValue($value, $record); - } + // Set value using parent + return parent::setValue($value, $record); + } 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(); + } public function saveInto(DataObjectInterface $record) { - // Check required relation details are available - $name = $this->getName(); - if (!$name) { - return $this; - } - $value = $this->Value(); - foreach (array('Filename', 'Hash', 'Variant') as $part) { - $partValue = isset($value[$part]) - ? $value[$part] - : null; - $record->setField("{$name}{$part}", $partValue); - } - return $this; - } + // Check required relation details are available + $name = $this->getName(); + if(!$name) { + return $this; + } + $value = $this->Value(); + foreach(array('Filename', 'Hash', 'Variant') as $part) { + $partValue = isset($value[$part]) + ? $value[$part] + : null; + $record->setField("{$name}{$part}", $partValue); + } + return $this; + } - /** - * Assign a front-end config variable for the upload field - * - * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available - * - * @param string $key - * @param mixed $val - * @return $this self reference - */ + /** + * Assign a front-end config variable for the upload field + * + * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available + * + * @param string $key + * @param mixed $val + * @return $this self reference + */ public function setConfig($key, $val) { - $this->ufConfig[$key] = $val; - return $this; - } + $this->ufConfig[$key] = $val; + return $this; + } - /** - * Gets a front-end config variable for the upload field - * - * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available - * - * @param string $key - * @return mixed - */ + /** + * Gets a front-end config variable for the upload field + * + * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available + * + * @param string $key + * @return mixed + */ public function getConfig($key) { - if (isset($this->ufConfig[$key])) { - return $this->ufConfig[$key]; - } - } + if(isset($this->ufConfig[$key])) { + return $this->ufConfig[$key]; + } + } - /** - * Determine if the field should automatically upload the file. - * - * @return boolean - */ + /** + * Determine if the field should automatically upload the file. + * + * @return boolean + */ public function getAutoUpload() { - return $this->getConfig('autoUpload'); - } + return $this->getConfig('autoUpload'); + } - /** - * Determine if the field should automatically upload the file - * - * @param boolean $autoUpload - * @return $this Self reference - */ + /** + * Determine if the field should automatically upload the file + * + * @param boolean $autoUpload + * @return $this Self reference + */ public function setAutoUpload($autoUpload) { - return $this->setConfig('autoUpload', $autoUpload); - } + return $this->setConfig('autoUpload', $autoUpload); + } - /** - * Determine if the user has permission to upload. - * - * @return boolean - */ + /** + * Determine if the user has permission to upload. + * + * @return boolean + */ public function canUpload() { - if (!$this->isActive()) { - return false; - } - $can = $this->getConfig('canUpload'); - if (is_bool($can)) { - return $can; - } - return Permission::check($can); - } + if(!$this->isActive()) { + return false; + } + $can = $this->getConfig('canUpload'); + if(is_bool($can)) { + return $can; + } + return Permission::check($can); + } - /** - * Specify whether the user can upload files. - * String values will be treated as required permission codes - * - * @param bool|string $canUpload Either a boolean flag, or a required - * permission code - * @return $this Self reference - */ + /** + * Specify whether the user can upload files. + * String values will be treated as required permission codes + * + * @param bool|string $canUpload Either a boolean flag, or a required + * permission code + * @return $this Self reference + */ public function setCanUpload($canUpload) { - return $this->setConfig('canUpload', $canUpload); - } + return $this->setConfig('canUpload', $canUpload); + } - /** - * Returns true if the field is neither readonly nor disabled - * - * @return bool - */ + /** + * Returns true if the field is neither readonly nor disabled + * + * @return bool + */ public function isActive() { - return !$this->isDisabled() && !$this->isReadonly(); - } + return !$this->isDisabled() && !$this->isReadonly(); + } - /** - * Gets thumbnail width. Defaults to 80 - * - * @return int - */ + /** + * Gets thumbnail width. Defaults to 80 + * + * @return int + */ public function getPreviewMaxWidth() { - return $this->getConfig('previewMaxWidth'); - } + return $this->getConfig('previewMaxWidth'); + } - /** - * Set thumbnail width. - * - * @param int $previewMaxWidth - * @return $this Self reference - */ + /** + * Set thumbnail width. + * + * @param int $previewMaxWidth + * @return $this Self reference + */ public function setPreviewMaxWidth($previewMaxWidth) { - return $this->setConfig('previewMaxWidth', $previewMaxWidth); - } + return $this->setConfig('previewMaxWidth', $previewMaxWidth); + } - /** - * Gets thumbnail height. Defaults to 60 - * - * @return int - */ + /** + * Gets thumbnail height. Defaults to 60 + * + * @return int + */ public function getPreviewMaxHeight() { - return $this->getConfig('previewMaxHeight'); - } + return $this->getConfig('previewMaxHeight'); + } - /** - * Set thumbnail height. - * - * @param int $previewMaxHeight - * @return $this Self reference - */ + /** + * Set thumbnail height. + * + * @param int $previewMaxHeight + * @return $this Self reference + */ public function setPreviewMaxHeight($previewMaxHeight) { - return $this->setConfig('previewMaxHeight', $previewMaxHeight); - } + return $this->setConfig('previewMaxHeight', $previewMaxHeight); + } - /** - * javascript template used to display uploading files - * Defaults to 'ss-uploadfield-uploadtemplate' - * - * @see javascript/UploadField_uploadtemplate.js - * @return string - */ + /** + * javascript template used to display uploading files + * Defaults to 'ss-uploadfield-uploadtemplate' + * + * @see javascript/UploadField_uploadtemplate.js + * @return string + */ public function getUploadTemplateName() { - return $this->getConfig('uploadTemplateName'); - } + return $this->getConfig('uploadTemplateName'); + } - /** - * Set javascript template used to display uploading files - * - * @param string $uploadTemplateName - * @return $this Self reference - */ + /** + * Set javascript template used to display uploading files + * + * @param string $uploadTemplateName + * @return $this Self reference + */ public function setUploadTemplateName($uploadTemplateName) { - return $this->setConfig('uploadTemplateName', $uploadTemplateName); - } + return $this->setConfig('uploadTemplateName', $uploadTemplateName); + } - /** - * javascript template used to display already uploaded files - * Defaults to 'ss-downloadfield-downloadtemplate' - * - * @see javascript/DownloadField_downloadtemplate.js - * @return string - */ + /** + * javascript template used to display already uploaded files + * Defaults to 'ss-downloadfield-downloadtemplate' + * + * @see javascript/DownloadField_downloadtemplate.js + * @return string + */ public function getDownloadTemplateName() { - return $this->getConfig('downloadTemplateName'); - } + return $this->getConfig('downloadTemplateName'); + } - /** - * Set javascript template used to display already uploaded files - * - * @param string $downloadTemplateName - * @return $this Self reference - */ + /** + * Set javascript template used to display already uploaded files + * + * @param string $downloadTemplateName + * @return $this Self reference + */ public function setDownloadTemplateName($downloadTemplateName) { - return $this->setConfig('downloadTemplateName', $downloadTemplateName); - } + return $this->setConfig('downloadTemplateName', $downloadTemplateName); + } public function extraClass() { - if ($this->isDisabled()) { - $this->addExtraClass('disabled'); - } - if ($this->isReadonly()) { - $this->addExtraClass('readonly'); - } + if($this->isDisabled()) { + $this->addExtraClass('disabled'); + } + if($this->isReadonly()) { + $this->addExtraClass('readonly'); + } - return parent::extraClass(); - } + return parent::extraClass(); + } public function Field($properties = array()) { - // Calculated config as per jquery.fileupload-ui.js - $config = array( - 'allowedMaxFileNumber' => 1, // Only one file allowed for AssetField - 'url' => $this->Link('upload'), - 'urlSelectDialog' => $this->Link('select'), - 'urlAttach' => $this->Link('attach'), - 'urlFileExists' => $this->link('fileexists'), - 'acceptFileTypes' => '.+$', - // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed - 'maxNumberOfFiles' => $this->Value() ? 0 : 1, - 'replaceFile' => false, // Should always be false for AssetField - ); + // Calculated config as per jquery.fileupload-ui.js + $config = array( + 'allowedMaxFileNumber' => 1, // Only one file allowed for AssetField + 'url' => $this->Link('upload'), + 'urlSelectDialog' => $this->Link('select'), + 'urlAttach' => $this->Link('attach'), + 'urlFileExists' => $this->link('fileexists'), + 'acceptFileTypes' => '.+$', + // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed + 'maxNumberOfFiles' => $this->Value() ? 0 : 1, + 'replaceFile' => false, // Should always be false for AssetField + ); - // Validation: File extensions - if ($allowedExtensions = $this->getAllowedExtensions()) { - $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; - $config['errorMessages']['acceptFileTypes'] = _t( - 'File.INVALIDEXTENSIONSHORT', - 'Extension is not allowed' - ); - } + // Validation: File extensions + if ($allowedExtensions = $this->getAllowedExtensions()) { + $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; + $config['errorMessages']['acceptFileTypes'] = _t( + 'File.INVALIDEXTENSIONSHORT', + 'Extension is not allowed' + ); + } - // Validation: File size - if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) { - $config['maxFileSize'] = $allowedMaxFileSize; - $config['errorMessages']['maxFileSize'] = _t( - 'File.TOOLARGESHORT', - 'Filesize exceeds {size}', - array('size' => File::format_size($config['maxFileSize'])) - ); - } + // Validation: File size + if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) { + $config['maxFileSize'] = $allowedMaxFileSize; + $config['errorMessages']['maxFileSize'] = _t( + 'File.TOOLARGESHORT', + 'Filesize exceeds {size}', + array('size' => File::format_size($config['maxFileSize'])) + ); + } - $mergedConfig = array_merge($config, $this->ufConfig); - return $this->customise(array( - 'ConfigString' => Convert::raw2json($mergedConfig), - 'UploadFieldFileButtons' => $this->renderWith($this->getTemplateFileButtons()) - ))->renderWith($this->getTemplates()); - } + $mergedConfig = array_merge($config, $this->ufConfig); + return $this->customise(array( + 'ConfigString' => Convert::raw2json($mergedConfig), + 'UploadFieldFileButtons' => $this->renderWith($this->getTemplateFileButtons()) + ))->renderWith($this->getTemplates()); + } - /** - * Validation method for this field, called when the entire form is validated - * - * @param Validator $validator - * @return boolean - */ + /** + * Validation method for this field, called when the entire form is validated + * + * @param Validator $validator + * @return boolean + */ public function validate($validator) { - $name = $this->getName(); - $value = $this->Value(); + $name = $this->getName(); + $value = $this->Value(); - // If there is no file then quit - if (!$value) { - return true; - } + // If there is no file then quit + if(!$value) { + return true; + } - // Revalidate each file against nested validator - $this->getUpload()->clearErrors(); + // Revalidate each file against nested validator + $this->getUpload()->clearErrors(); - // Generate $_FILES style file attribute array for upload validator - $store = $this->getAssetStore(); - $mime = $store->getMimeType($value['Filename'], $value['Hash'], $value['Variant']); - $metadata = $store->getMetadata($value['Filename'], $value['Hash'], $value['Variant']); - $tmpFile = array( - 'name' => $value['Filename'], - 'type' => $mime, - 'size' => isset($metadata['size']) ? $metadata['size'] : 0, - 'tmp_name' => null, // Should bypass is_uploaded_file check - 'error' => UPLOAD_ERR_OK, - ); - $this->getUpload()->validate($tmpFile); + // Generate $_FILES style file attribute array for upload validator + $store = $this->getAssetStore(); + $mime = $store->getMimeType($value['Filename'], $value['Hash'], $value['Variant']); + $metadata = $store->getMetadata($value['Filename'], $value['Hash'], $value['Variant']); + $tmpFile = array( + 'name' => $value['Filename'], + 'type' => $mime, + 'size' => isset($metadata['size']) ? $metadata['size'] : 0, + 'tmp_name' => null, // Should bypass is_uploaded_file check + 'error' => UPLOAD_ERR_OK, + ); + $this->getUpload()->validate($tmpFile); - // Check all errors - if ($errors = $this->getUpload()->getErrors()) { - foreach ($errors as $error) { - $validator->validationError($name, $error, "validation"); - } - return false; - } + // Check all errors + if($errors = $this->getUpload()->getErrors()) { + foreach($errors as $error) { + $validator->validationError($name, $error, "validation"); + } + return false; + } - return true; - } + return true; + } - /** - * Given an array of post variables, extract all temporary file data into an array - * - * @param array $postVars Array of posted form data - * @return array data for uploaded file - */ + /** + * Given an array of post variables, extract all temporary file data into an array + * + * @param array $postVars Array of posted form data + * @return array data for uploaded file + */ protected function extractUploadedFileData($postVars) { - // Note: Format of posted file parameters in php is a feature of using - // for multiple file uploads + // Note: Format of posted file parameters in php is a feature of using + // for multiple file uploads - // Skip empty file - if (empty($postVars['tmp_name'])) { - return null; - } + // Skip empty file + if(empty($postVars['tmp_name'])) { + return null; + } - // Return single level array for posted file - /** @skipUpgrade */ - if (empty($postVars['tmp_name']['Upload'])) { - return $postVars; - } + // Return single level array for posted file + /** @skipUpgrade */ + if(empty($postVars['tmp_name']['Upload'])) { + return $postVars; + } - // Extract posted feedback value - $tmpFile = array(); - foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $field) { - /** @skipUpgrade */ - $tmpFile[$field] = $postVars[$field]['Upload']; - } - return $tmpFile; - } + // Extract posted feedback value + $tmpFile = array(); + foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) { + /** @skipUpgrade */ + $tmpFile[$field] = $postVars[$field]['Upload']; + } + return $tmpFile; + } - /** - * Loads the temporary file data into the asset store, and return the tuple details - * for the result. - * - * @param array $tmpFile Temporary file data - * @param string $error Error message - * @return array Result of saved file, or null if error - */ + /** + * Loads the temporary file data into the asset store, and return the tuple details + * for the result. + * + * @param array $tmpFile Temporary file data + * @param string $error Error message + * @return array Result of saved file, or null if error + */ protected function saveTemporaryFile($tmpFile, &$error = null) { - $error = null; - if (empty($tmpFile)) { - $error = _t('UploadField.FIELDNOTSET', 'File information not found'); - return null; - } + $error = null; + if (empty($tmpFile)) { + $error = _t('UploadField.FIELDNOTSET', 'File information not found'); + return null; + } - if ($tmpFile['error']) { - $error = $tmpFile['error']; - return null; - } + if($tmpFile['error']) { + $error = $tmpFile['error']; + return null; + } - // Get the uploaded file into a new file object. - try { - $result = $this - ->getUpload() - ->load($tmpFile, $this->getFolderName()); - } catch (Exception $e) { - // we shouldn't get an error here, but just in case - $error = $e->getMessage(); - return null; - } + // Get the uploaded file into a new file object. + try { + $result = $this + ->getUpload() + ->load($tmpFile, $this->getFolderName()); + } catch (Exception $e) { + // we shouldn't get an error here, but just in case + $error = $e->getMessage(); + return null; + } - // Check if upload field has an error - if ($this->getUpload()->isError()) { - $error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors()); - return null; - } + // Check if upload field has an error + if ($this->getUpload()->isError()) { + $error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors()); + return null; + } - // return tuple array of Filename, Hash and Variant - return $result; - } + // return tuple array of Filename, Hash and Variant + return $result; + } - /** - * Safely encodes the File object with all standard fields required - * by the front end - * - * @param string $filename - * @param string $hash - * @param string $variant - * @return array Encoded list of file attributes - */ + /** + * Safely encodes the File object with all standard fields required + * by the front end + * + * @param string $filename + * @param string $hash + * @param string $variant + * @return array Encoded list of file attributes + */ protected function encodeAssetAttributes($filename, $hash, $variant) { - // Force regeneration of file thumbnail for this tuple (without saving into db) - $object = DBFile::create(); - $object->setValue(array('Filename' => $filename, 'Hash' => $hash, 'Variant' => $variant)); + // Force regeneration of file thumbnail for this tuple (without saving into db) + $object = DBFile::create(); + $object->setValue(array('Filename' => $filename, 'Hash' => $hash, 'Variant' => $variant)); - return array( - 'filename' => $filename, - 'hash' => $hash, - 'variant' => $variant, - 'name' => $object->getBasename(), - 'url' => $object->getURL(), - 'thumbnail_url' => $object->ThumbnailURL( - $this->getPreviewMaxWidth(), - $this->getPreviewMaxHeight() - ), - 'size' => $object->getAbsoluteSize(), - 'type' => File::get_file_type($object->getFilename()), - 'buttons' => (string)$this->renderWith($this->getTemplateFileButtons()), - 'fieldname' => $this->getName() - ); - } + return array( + 'filename' => $filename, + 'hash' => $hash, + 'variant' => $variant, + 'name' => $object->getBasename(), + 'url' => $object->getURL(), + 'thumbnail_url' => $object->ThumbnailURL( + $this->getPreviewMaxWidth(), + $this->getPreviewMaxHeight() + ), + 'size' => $object->getAbsoluteSize(), + 'type' => File::get_file_type($object->getFilename()), + 'buttons' => (string)$this->renderWith($this->getTemplateFileButtons()), + 'fieldname' => $this->getName() + ); + } - /** - * Action to handle upload of a single file - * - * @param HTTPRequest $request - * @return HTTPResponse - */ + /** + * Action to handle upload of a single file + * + * @param HTTPRequest $request + * @return HTTPResponse + */ public function upload(HTTPRequest $request) { - if ($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { - return $this->httpError(403); - } + if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { + return $this->httpError(403); + } - // Protect against CSRF on destructive action - $token = $this - ->getForm() - ->getSecurityToken(); - if (!$token->checkRequest($request)) { - return $this->httpError(400); - } + // Protect against CSRF on destructive action + $token = $this + ->getForm() + ->getSecurityToken(); + if(!$token->checkRequest($request)) { + return $this->httpError(400); + } - // Get form details - $name = $this->getName(); - $postVars = $request->postVar($name); + // Get form details + $name = $this->getName(); + $postVars = $request->postVar($name); - // Extract uploaded files from Form data - $uploadedFile = $this->extractUploadedFileData($postVars); - if (!$uploadedFile) { - return $this->httpError(400); - } + // Extract uploaded files from Form data + $uploadedFile = $this->extractUploadedFileData($postVars); + if(!$uploadedFile) { + return $this->httpError(400); + } - // Save the temporary files into a File objects - // and save data/error on a per file basis - $result = $this->saveTemporaryFile($uploadedFile, $error); - if (empty($result)) { - $return = array('error' => $error); - } else { - $return = $this->encodeAssetAttributes($result['Filename'], $result['Hash'], $result['Variant']); - } - $this - ->getUpload() - ->clearErrors(); + // Save the temporary files into a File objects + // and save data/error on a per file basis + $result = $this->saveTemporaryFile($uploadedFile, $error); + if(empty($result)) { + $return = array('error' => $error); + } else { + $return = $this->encodeAssetAttributes($result['Filename'], $result['Hash'], $result['Variant']); + } + $this + ->getUpload() + ->clearErrors(); - // Format response with json - $response = new HTTPResponse(Convert::raw2json(array($return))); - $response->addHeader('Content-Type', 'text/plain'); - return $response; - } + // Format response with json + $response = new HTTPResponse(Convert::raw2json(array($return))); + $response->addHeader('Content-Type', 'text/plain'); + return $response; + } public function performReadonlyTransformation() { - $clone = clone $this; - $clone->addExtraClass('readonly'); - $clone->setReadonly(true); - return $clone; - } + $clone = clone $this; + $clone->addExtraClass('readonly'); + $clone->setReadonly(true); + return $clone; + } - /** - * Gets the foreign class that needs to be created, or 'File' as default if there - * is no relationship, or it cannot be determined. - * - * @param string $default Default value to return if no value could be calculated - * @return string Foreign class name. - */ + /** + * 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 + // 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; - } - } + // 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 - */ + /** + * @return AssetStore + */ protected function getAssetStore() { - return Injector::inst()->get('AssetStore'); - } + return Injector::inst()->get('AssetStore'); + } + public function getAttributes() { + return array_merge( + parent::getAttributes(), + ['type' => 'file'] + ); + } + } diff --git a/src/Forms/FileField.php b/src/Forms/FileField.php index b54cbda62..6f61a8759 100644 --- a/src/Forms/FileField.php +++ b/src/Forms/FileField.php @@ -2,10 +2,8 @@ namespace SilverStripe\Forms; -use SilverStripe\Assets\Upload_Validator; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; -use SilverStripe\Assets\Upload; use SilverStripe\Assets\File; use SilverStripe\Core\Object; @@ -27,273 +25,156 @@ use SilverStripe\Core\Object; * * class ExampleForm_Controller extends Page_Controller { * - * function Form() { - * $fields = new FieldList( - * new TextField('MyName'), - * new FileField('MyFile') - * ); - * $actions = new FieldList( - * new FormAction('doUpload', 'Upload file') - * ); + * function Form() { + * $fields = new FieldList( + * new TextField('MyName'), + * new FileField('MyFile') + * ); + * $actions = new FieldList( + * new FormAction('doUpload', 'Upload file') + * ); * $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) { - * $file = $data['MyFile']; - * $content = file_get_contents($file['tmp_name']); - * // ... process content - * } + * function doUpload($data, $form) { + * $file = $data['MyFile']; + * $content = file_get_contents($file['tmp_name']); + * // ... process content + * } * } * */ -class FileField extends FormField -{ +class FileField extends FormField { + use Uploadable; - /** - * Flag to automatically determine and save a has_one-relationship - * on the saved record (e.g. a "Player" has_one "PlayerImage" would - * trigger saving the ID of newly created file into "PlayerImageID" - * on the record). - * - * @var boolean - */ - public $relationAutoSetting = true; + /** + * Flag to automatically determine and save a has_one-relationship + * on the saved record (e.g. a "Player" has_one "PlayerImage" would + * trigger saving the ID of newly created file into "PlayerImageID" + * on the record). + * + * @var boolean + */ + protected $relationAutoSetting = true; - /** - * Upload object (needed for validation - * and actually moving the temporary file - * created by PHP). - * - * @var Upload - */ - protected $upload; + /** + * Create a new file field. + * + * @param string $name The internal field name, passed to forms. + * @param string $title The field label. + * @param int $value The value of the field. + */ + public function __construct($name, $title = null, $value = null) { + $this->constructUploadable(); + parent::__construct($name, $title, $value); + } - /** - * Partial filesystem path relative to /assets directory. - * Defaults to Upload::$uploads_folder. - * - * @var string - */ - protected $folderName = false; + /** + * @param array $properties + * @return string + */ + public function Field($properties = array()) { + $properties = array_merge($properties, array( + 'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize() + )); - /** - * Create a new file field. - * - * @param string $name The internal field name, passed to forms. - * @param string $title The field label. - * @param int $value The value of the field. - */ - public function __construct($name, $title = null, $value = null) - { - $this->upload = Upload::create(); - parent::__construct($name, $title, $value); - } + return parent::Field($properties); + } - /** - * @param array $properties - * @return string - */ - public function Field($properties = array()) - { - $properties = array_merge($properties, array( - 'MaxFileSize' => $this->getValidator()->getAllowedMaxFileSize() - )); + public function getAttributes() { + return array_merge( + parent::getAttributes(), + array('type' => 'file') + ); + } - return parent::Field($properties); - } + /** + * @param DataObject|DataObjectInterface $record + */ + public function saveInto(DataObjectInterface $record) { + if(!isset($_FILES[$this->name])) { + return; + } - public function getAttributes() - { - return array_merge( - parent::getAttributes(), - array('type' => 'file') - ); - } + $fileClass = File::get_class_for_file_extension( + File::get_file_extension($_FILES[$this->name]['name']) + ); - /** - * @param DataObject|DataObjectInterface $record - */ - public function saveInto(DataObjectInterface $record) - { - if (!isset($_FILES[$this->name])) { - return; - } + /** @var File $file */ + if($this->relationAutoSetting) { + // assume that the file is connected via a has-one + $objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name); + if($objectClass === File::class || empty($objectClass)) { + // Create object of the appropriate file class + $file = Object::create($fileClass); + } else { + // try to create a file matching the relation + $file = Object::create($objectClass); + } + } else if($record instanceof File) { + $file = $record; + } else { + $file = Object::create($fileClass); + } - $fileClass = File::get_class_for_file_extension( - File::get_file_extension($_FILES[$this->name]['name']) - ); + $this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName()); - /** @var File $file */ - if ($this->relationAutoSetting) { - // assume that the file is connected via a has-one - $objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name); - if ($objectClass === File::class || empty($objectClass)) { - // Create object of the appropriate file class - $file = Object::create($fileClass); - } else { - // try to create a file matching the relation - $file = Object::create($objectClass); - } - } elseif ($record instanceof File) { - $file = $record; - } else { - $file = Object::create($fileClass); - } + if($this->upload->isError()) { + return; + } - $this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName()); + if($this->relationAutoSetting) { + if (empty($objectClass)) { + return; + } - if ($this->upload->isError()) { - return; - } + $file = $this->upload->getFile(); - if ($this->relationAutoSetting) { - if (empty($objectClass)) { - return; - } + $record->{$this->name . 'ID'} = $file->ID; + } + } - $file = $this->upload->getFile(); + public function Value() { + return isset($_FILES[$this->getName()]) ? $_FILES[$this->getName()] : null; + } - $record->{$this->name . 'ID'} = $file->ID; - } - } + public function validate($validator) { + if(!isset($_FILES[$this->name])) return true; - public function Value() - { - return isset($_FILES[$this->getName()]) ? $_FILES[$this->getName()] : null; - } + $tmpFile = $_FILES[$this->name]; - /** - * Get custom validator for this field - * - * @return Upload_Validator - */ - public function getValidator() - { - return $this->upload->getValidator(); - } + $valid = $this->upload->validate($tmpFile); + if(!$valid) { + $errors = $this->upload->getErrors(); + if($errors) foreach($errors as $error) { + $validator->validationError($this->name, $error, "validation"); + } + return false; + } - /** - * Set custom validator for this field - * - * @param Upload_Validator $validator - * @return $this Self reference - */ - public function setValidator($validator) - { - $this->upload->setValidator($validator); - return $this; - } + return true; + } - /** - * Sets the upload folder name - * - * @param string $folderName - * @return FileField Self reference - */ - public function setFolderName($folderName) - { - $this->folderName = $folderName; - return $this; - } + /** + * Set if relation can be automatically assigned to the underlying dataobject + * + * @param bool $auto + * @return $this + */ + public function setRelationAutoSetting($auto) { + $this->relationAutoSetting = $auto; + return $this; + } - /** - * Gets the upload folder name - * - * @return string - */ - public function getFolderName() - { - return ($this->folderName !== false) - ? $this->folderName - : Upload::config()->uploads_folder; - } + /** + * Check if relation can be automatically assigned to the underlying dataobject + * + * @return bool + */ + public function getRelationAutoSetting() { + return $this->relationAutoSetting; + } - public function validate($validator) - { - if (!isset($_FILES[$this->name])) { - return true; - } - - $tmpFile = $_FILES[$this->name]; - - $valid = $this->upload->validate($tmpFile); - if (!$valid) { - $errors = $this->upload->getErrors(); - if ($errors) { - foreach ($errors as $error) { - $validator->validationError($this->name, $error, "validation"); - } - } - return false; - } - - return true; - } - - /** - * Retrieves the Upload handler - * - * @return Upload - */ - public function getUpload() - { - return $this->upload; - } - - /** - * Sets the upload handler - * - * @param Upload $upload - * @return FileField Self reference - */ - public function setUpload(Upload $upload) - { - $this->upload = $upload; - return $this; - } - - /** - * Limit allowed file extensions. Empty by default, allowing all extensions. - * To allow files without an extension, use an empty string. - * See {@link File::$allowed_extensions} to get a good standard set of - * extensions that are typically not harmful in a webserver context. - * See {@link setAllowedMaxFileSize()} to limit file size by extension. - * - * @param array $rules List of extensions - * @return $this - */ - public function setAllowedExtensions($rules) - { - $this->getValidator()->setAllowedExtensions($rules); - return $this; - } - - /** - * Limit allowed file extensions by specifying categories of file types. - * These may be 'image', 'image/supported', 'audio', 'video', 'archive', 'flash', or 'document' - * See {@link File::$allowed_extensions} for details of allowed extensions - * for each of these categories - * - * @param string $category Category name - * @param string,... $categories Additional category names - * @return $this - */ - public function setAllowedFileCategories($category) - { - $extensions = File::get_category_extensions(func_get_args()); - return $this->setAllowedExtensions($extensions); - } - - /** - * Returns list of extensions allowed by this field, or an empty array - * if there is no restriction - * - * @return array - */ - public function getAllowedExtensions() - { - return $this->getValidator()->getAllowedExtensions(); - } } diff --git a/src/Forms/FileUploadable.php b/src/Forms/FileUploadable.php new file mode 100644 index 000000000..e67803e49 --- /dev/null +++ b/src/Forms/FileUploadable.php @@ -0,0 +1,399 @@ +constructUploadable(); + } + + + /** + * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) + * + * @param DataObject $record + * @return $this + */ + public function setRecord($record) { + $this->record = $record; + return $this; + } + /** + * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will + * use Form->getRecord() or Form->Controller()->data() + * + * @return DataObject + */ + public function getRecord() { + if ($this->record) { + return $this->record; + } + if (!$this->getForm()) { + return null; + } + + // Get record from form + $record = $this->getForm()->getRecord(); + if ($record && ($record instanceof DataObject)) { + $this->record = $record; + return $record; + } + + // Get record from controller + $controller = $this->getForm()->getController(); + if ($controller + && $controller->hasMethod('data') + && ($record = $controller->data()) + && ($record instanceof DataObject) + ) { + $this->record = $record; + return $record; + } + + return null; + } + + + /** + * Loads the related record values into this field. This can be uploaded + * in one of three ways: + * + * - 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). + * - By passing in an explicit list of File objects in the $record parameter, and + * leaving $value blank. + * - By passing in a dataobject in the $record parameter, from which file objects + * 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 + * field value (list of file ID values). + * + * @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, + * SS_List of items, or an array of submitted form data + * @return $this Self reference + * @throws ValidationException + */ + public function setValue($value, $record = null) { + + // If we're not passed a value directly, we can attempt to infer the field + // value from the second parameter by inspecting its relations + $items = new ArrayList(); + + // Determine format of presented data + if(empty($value) && $record) { + // If a record is given as a second parameter, but no submitted values, + // then we should inspect this instead for the form values + + if(($record instanceof DataObject) && $record->hasMethod($this->getName())) { + // If given a dataobject use reflection to extract details + + $data = $record->{$this->getName()}(); + if($data instanceof DataObject) { + // If has_one, add sole item to default list + $items->push($data); + } elseif($data instanceof SS_List) { + // For many_many and has_many relations we can use the relation list directly + $items = $data; + } + } elseif($record instanceof SS_List) { + // If directly passing a list then save the items directly + $items = $record; + } + } elseif(!empty($value['Files'])) { + // If value is given as an array (such as a posted form), extract File IDs from this + $class = $this->getRelationAutosetClass(); + $items = DataObject::get($class)->byIDs($value['Files']); + } + + // If javascript is disabled, direct file upload (non-html5 style) can + // trigger a single or multiple file submission. Note that this may be + // included in addition to re-submitted File IDs as above, so these + // should be added to the list instead of operated on independently. + if($uploadedFiles = $this->extractUploadedFileData($value)) { + foreach($uploadedFiles as $tempFile) { + $file = $this->saveTemporaryFile($tempFile, $error); + if($file) { + $items->add($file); + } else { + throw new ValidationException($error); + } + } + } + + // Filter items by what's allowed to be viewed + $filteredItems = new ArrayList(); + $fileIDs = array(); + foreach($items as $file) { + if($file->exists() && $file->canView()) { + $filteredItems->push($file); + $fileIDs[] = $file->ID; + } + } + + // Filter and cache updated item list + $this->items = $filteredItems; + // Same format as posted form values for this field. Also ensures that + // $this->setValue($this->getValue()); is non-destructive + $value = $fileIDs ? array('Files' => $fileIDs) : null; + + // Set value using parent + parent::setValue($value, $record); + return $this; + } + + /** + * 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 + * updating the internal list of File items. + * + * @param SS_List $items + * @return $this self reference + */ + public function setItems(SS_List $items) { + return $this->setValue(null, $items); + } + + /** + * Retrieves the current list of files + * + * @return SS_List|File[] + */ + public function getItems() { + return $this->items ? $this->items : new ArrayList(); + } + + /** + * Retrieves the list of selected file IDs + * + * @return array + */ + public function getItemIDs() { + $value = $this->Value(); + return empty($value['Files']) ? array() : $value['Files']; + } + + public function Value() { + // Re-override FileField Value to use data value + return $this->dataValue(); + } + + /** + * @param DataObject|DataObjectInterface $record + * @return $this + */ + public function saveInto(DataObjectInterface $record) { + // Check required relation details are available + $fieldname = $this->getName(); + if(!$fieldname) { + return $this; + } + + // Get details to save + $idList = $this->getItemIDs(); + + // Check type of relation + $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; + if($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) { + // has_many or many_many + $relation->setByIDList($idList); + } elseif($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) { + // Assign has_one ID + $id = $idList ? reset($idList) : 0; + $record->{"{$fieldname}ID"} = $id; + + // Polymorphic asignment + if ($class === DataObject::class) { + $file = $id ? File::get()->byID($id) : null; + $fileClass = $file ? get_class($file) : File::class; + $record->{"{$fieldname}Class"} = $id ? $fileClass : null; + } + } + return $this; + } + + /** + * Loads the temporary file data into a File object + * + * @param array $tmpFile Temporary file data + * @param string $error Error message + * @return AssetContainer File object, or null if error + */ + protected function saveTemporaryFile($tmpFile, &$error = null) { + // Determine container object + $error = null; + $fileObject = null; + + if (empty($tmpFile)) { + $error = _t('UploadField.FIELDNOTSET', 'File information not found'); + return null; + } + + if($tmpFile['error']) { + $error = $tmpFile['error']; + return null; + } + + // Search for relations that can hold the uploaded files, but don't fallback + // to default if there is no automatic relation + if ($relationClass = $this->getRelationAutosetClass(null)) { + // Allow File to be subclassed + if($relationClass === File::class && isset($tmpFile['name'])) { + $relationClass = File::get_class_for_file_extension( + File::get_file_extension($tmpFile['name']) + ); + } + // Create new object explicitly. Otherwise rely on Upload::load to choose the class. + $fileObject = Object::create($relationClass); + if(! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) { + throw new InvalidArgumentException("Invalid asset container $relationClass"); + } + } + + // Get the uploaded file into a new file object. + try { + $this->getUpload()->loadIntoFile($tmpFile, $fileObject, $this->getFolderName()); + } catch (Exception $e) { + // we shouldn't get an error here, but just in case + $error = $e->getMessage(); + return null; + } + + // Check if upload field has an error + if ($this->getUpload()->isError()) { + $error = implode(' ' . PHP_EOL, $this->getUpload()->getErrors()); + return null; + } + + // return file + return $this->getUpload()->getFile(); + } + + /** + * 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 = File::class) { + // Don't autodetermine relation if no relationship between parent record + if(!$this->getRelationAutoSetting()) { + 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; + } + } + + /** + * Set if relation can be automatically assigned to the underlying dataobject + * + * @param bool $auto + * @return $this + */ + public function setRelationAutoSetting($auto) { + $this->relationAutoSetting = $auto; + return $this; + } + + /** + * Check if relation can be automatically assigned to the underlying dataobject + * + * @return bool + */ + public function getRelationAutoSetting() { + return $this->relationAutoSetting; + } + + /** + * Given an array of post variables, extract all temporary file data into an array + * + * @param array $postVars Array of posted form data + * @return array List of temporary file data + */ + protected function extractUploadedFileData($postVars) { + // Note: Format of posted file parameters in php is a feature of using + // for multiple file uploads + $tmpFiles = array(); + if( !empty($postVars['tmp_name']) + && is_array($postVars['tmp_name']) + && !empty($postVars['tmp_name']['Uploads']) + ) { + for($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) { + // Skip if "empty" file + if(empty($postVars['tmp_name']['Uploads'][$i])) { + continue; + } + $tmpFile = array(); + foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) { + $tmpFile[$field] = $postVars[$field]['Uploads'][$i]; + } + $tmpFiles[] = $tmpFile; + } + } elseif(!empty($postVars['tmp_name'])) { + // Fallback to allow single file uploads (method used by AssetUploadField) + $tmpFiles[] = $postVars; + } + + return $tmpFiles; + } +} diff --git a/src/Forms/UploadField.php b/src/Forms/UploadField.php index cdaefb68d..904e94871 100644 --- a/src/Forms/UploadField.php +++ b/src/Forms/UploadField.php @@ -14,11 +14,7 @@ use SilverStripe\ORM\SS_List; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ValidationException; -use SilverStripe\ORM\DataObjectInterface; -use SilverStripe\ORM\RelationList; -use SilverStripe\ORM\UnsavedRelationList; use SilverStripe\Security\Permission; -use SilverStripe\View\Requirements; use SilverStripe\View\ArrayData; use SilverStripe\View\ViewableData; use SilverStripe\View\ViewableData_Customised; @@ -49,1396 +45,1000 @@ use Exception; * 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. */ -class UploadField extends FileField -{ +class UploadField extends FormField { + use FileUploadable; - /** - * @var array - */ - private static $allowed_actions = array( - 'upload', - 'attach', - 'handleItem', - 'handleSelect', - 'fileexists' - ); + /** + * @var array + */ + private static $allowed_actions = array( + 'upload', + 'attach', + 'handleItem', + 'handleSelect', + 'fileexists' + ); - /** - * @var array - */ - private static $url_handlers = array( - 'item/$ID' => 'handleItem', - 'select' => 'handleSelect', - '$Action!' => '$Action', - ); + /** + * @var array + */ + private static $url_handlers = array( + 'item/$ID' => 'handleItem', + 'select' => 'handleSelect', + '$Action!' => '$Action', + ); - /** - * Template to use for the file button widget - * - * @var string - */ - protected $templateFileButtons = null; + /** + * Template to use for the file button widget + * + * @var string + */ + protected $templateFileButtons = null; - /** - * Template to use for the edit form - * - * @var string - */ - protected $templateFileEdit = null; + /** + * Template to use for the edit form + * + * @var string + */ + protected $templateFileEdit = null; - /** - * Parent data record. Will be infered from parent form or controller if blank. - * - * @var DataObject - */ - protected $record; + /** + * Config for this field used in the front-end javascript + * (will be merged into the config of the javascript file upload plugin). + * + * @var array + */ + protected $ufConfig = array(); - /** - * Items loaded into this field. May be a RelationList, or any other SS_List - * - * @var SS_List - */ - protected $items; + /** + * Front end config defaults + * + * @config + * @var array + */ + private static $defaultConfig = array( + /** + * Automatically upload the file once selected + * + * @var boolean + */ + 'autoUpload' => true, + /** + * 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. + * The resulting value will be set to maxNumberOfFiles + * + * @var integer + */ + 'allowedMaxFileNumber' => null, + /** + * Can the user upload new files, or just select from existing files. + * String values are interpreted as permission codes. + * + * @var boolean|string + */ + 'canUpload' => true, + /** + * Can the user attach files from the assets archive on the site? + * String values are interpreted as permission codes. + * + * @var boolean|string + */ + 'canAttachExisting' => "CMS_ACCESS_AssetAdmin", + /** + * Shows the target folder for new uploads in the field UI. + * Disable to keep the internal filesystem structure hidden from users. + * + * @var boolean|string + */ + 'canPreviewFolder' => true, + /** + * Indicate a change event to the containing form if an upload + * or file edit/delete was performed. + * + * @var boolean + */ + 'changeDetection' => true, + /** + * Maximum width of the preview thumbnail + * + * @var integer + */ + 'previewMaxWidth' => 80, + /** + * Maximum height of the preview thumbnail + * + * @var integer + */ + 'previewMaxHeight' => 60, + /** + * javascript template used to display uploading files + * + * @see javascript/UploadField_uploadtemplate.js + * @var string + */ + 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', + /** + * javascript template used to display already uploaded files + * + * @see javascript/UploadField_downloadtemplate.js + * @var string + */ + 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate', + /** + * 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 + * + * @see Upload + * @var boolean + */ + 'overwriteWarning' => true + ); - /** - * Config for this field used in the front-end javascript - * (will be merged into the config of the javascript file upload plugin). - * - * @var array - */ - protected $ufConfig = array(); + /** + * @var String Folder to display in "Select files" list. + * Defaults to listing all files regardless of folder. + * The folder path should be relative to the webroot. + * See {@link FileField->folderName} to set the upload target instead. + * @example admin/folder/subfolder + */ + protected $displayFolderName; - /** - * Front end config defaults - * - * @config - * @var array - */ - private static $defaultConfig = array( - /** - * Automatically upload the file once selected - * - * @var boolean - */ - 'autoUpload' => true, - /** - * 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. - * The resulting value will be set to maxNumberOfFiles - * - * @var integer - */ - 'allowedMaxFileNumber' => null, - /** - * Can the user upload new files, or just select from existing files. - * String values are interpreted as permission codes. - * - * @var boolean|string - */ - 'canUpload' => true, - /** - * Can the user attach files from the assets archive on the site? - * String values are interpreted as permission codes. - * - * @var boolean|string - */ - 'canAttachExisting' => "CMS_ACCESS_AssetAdmin", - /** - * Shows the target folder for new uploads in the field UI. - * Disable to keep the internal filesystem structure hidden from users. - * - * @var boolean|string - */ - 'canPreviewFolder' => true, - /** - * Indicate a change event to the containing form if an upload - * or file edit/delete was performed. - * - * @var boolean - */ - 'changeDetection' => true, - /** - * Maximum width of the preview thumbnail - * - * @var integer - */ - 'previewMaxWidth' => 80, - /** - * Maximum height of the preview thumbnail - * - * @var integer - */ - 'previewMaxHeight' => 60, - /** - * javascript template used to display uploading files - * - * @see javascript/UploadField_uploadtemplate.js - * @var string - */ - 'uploadTemplateName' => 'ss-uploadfield-uploadtemplate', - /** - * javascript template used to display already uploaded files - * - * @see javascript/UploadField_downloadtemplate.js - * @var string - */ - 'downloadTemplateName' => 'ss-uploadfield-downloadtemplate', - /** - * 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 - * - * @see Upload - * @var boolean - */ - 'overwriteWarning' => true - ); + /** + * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm + * @example 'getCMSFields' + * + * @var FieldList|string + */ + protected $fileEditFields = null; - /** - * @var String Folder to display in "Select files" list. - * Defaults to listing all files regardless of folder. - * The folder path should be relative to the webroot. - * See {@link FileField->folderName} to set the upload target instead. - * @example admin/folder/subfolder - */ - protected $displayFolderName; + /** + * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm + * @example 'getCMSActions' + * + * @var FieldList|string + */ + protected $fileEditActions = null; - /** - * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm - * @example 'getCMSFields' - * - * @var FieldList|string - */ - protected $fileEditFields = null; + /** + * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm + * @example 'getCMSValidator' + * + * @var RequiredFields|string + */ + protected $fileEditValidator = null; - /** - * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm - * @example 'getCMSActions' - * - * @var FieldList|string - */ - protected $fileEditActions = null; + /** + * Construct a new UploadField instance + * + * @param string $name The internal field name, passed to forms. + * @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 + * @link $record}, with the same name as the field name. + */ + 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 + $this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only - /** - * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm - * @example 'getCMSValidator' - * - * @var RequiredFields|string - */ - protected $fileEditValidator = null; + $this->ufConfig = self::config()->defaultConfig; + $this->constructFileUploadable(); - /** - * Construct a new UploadField instance - * - * @param string $name The internal field name, passed to forms. - * @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 - * @link $record}, with the same name as the field name. - */ - 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 - $this->addExtraClass('ss-uploadfield'); // class, used by css for uploadfield only - - $this->ufConfig = self::config()->defaultConfig; - - parent::__construct($name, $title); + parent::__construct($name, $title); if ($items) { $this->setItems($items); } + } + + /** + * Set name of template used for Buttons on each file (replace, edit, remove, delete) (without path or extension) + * + * @param string $template + * @return $this + */ + public function setTemplateFileButtons($template) { + $this->templateFileButtons = $template; + return $this; + } + + /** + * @return string + */ + 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 + * @return $this + */ + public function setTemplateFileEdit($template) { + $this->templateFileEdit = $template; + return $this; + } + + /** + * @return string + */ + public function getTemplateFileEdit() { + return $this->_templates($this->templateFileEdit, '_FileEdit'); + } + + /** + * Determine if the target folder for new uploads in is visible the field UI. + * + * @return boolean + */ + public function canPreviewFolder() { + if(!$this->isActive()) return false; + $can = $this->getConfig('canPreviewFolder'); + return (is_bool($can)) ? $can : Permission::check($can); + } + + /** + * Determine if the target folder for new uploads in is visible the field UI. + * Disable to keep the internal filesystem structure hidden from users. + * + * @param boolean|string $canPreviewFolder Either a boolean flag, or a + * required permission code + * @return UploadField Self reference + */ + public function setCanPreviewFolder($canPreviewFolder) { + return $this->setConfig('canPreviewFolder', $canPreviewFolder); + } + + /** + * 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) + * + * @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) + * + * @param boolean $overwriteWarning + * @return UploadField Self reference + */ + public function setOverwriteWarning($overwriteWarning) { + return $this->setConfig('overwriteWarning', $overwriteWarning); + } + + /** + * @param string $name + * @return $this + */ + public function setDisplayFolderName($name) { + $this->displayFolderName = $name; + return $this; + } + + /** + * @return String + */ + public function getDisplayFolderName() { + return $this->displayFolderName; + } + + + + /** + * Retrieves a customised list of all File records to ensure they are + * properly viewable when rendered in the field template. + * + * @return SS_List[ViewableData_Customised] + */ + public function getCustomisedItems() { + $customised = new ArrayList(); + foreach($this->getItems() as $file) { + $customised->push($this->customiseFile($file)); + } + return $customised; + } + + /** + * Customises a file with additional details suitable for rendering in the + * UploadField.ss template + * + * @param ViewableData|AssetContainer $file + * @return ViewableData_Customised + */ + protected function customiseFile(AssetContainer $file) { + $file = $file->customise(array( + 'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file), + 'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(), + 'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(), + 'UploadField' => $this + )); + // we do this in a second customise to have the access to the previous customisations + return $file->customise(array( + 'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons()) + )); + } + + /** + * Assign a front-end config variable for the upload field + * + * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available + * + * @param string $key + * @param mixed $val + * @return UploadField self reference + */ + public function setConfig($key, $val) { + $this->ufConfig[$key] = $val; + return $this; + } + + /** + * Gets a front-end config variable for the upload field + * + * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available + * + * @param string $key + * @return mixed + */ + public function getConfig($key) { + if(!isset($this->ufConfig[$key])) return null; + return $this->ufConfig[$key]; + } + + /** + * Determine if the field should automatically upload the file. + * + * @return boolean + */ + public function getAutoUpload() { + return $this->getConfig('autoUpload'); + } + + /** + * Determine if the field should automatically upload the file + * + * @param boolean $autoUpload + * @return UploadField Self reference + */ + public function setAutoUpload($autoUpload) { + return $this->setConfig('autoUpload', $autoUpload); + } + + /** + * 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 + */ + public function getAllowedMaxFileNumber() { + $allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber'); + + // if there is a has_one relation with that name on the record and + // allowedMaxFileNumber has not been set, it's wanted to be 1 + if(empty($allowedMaxFileNumber)) { + $record = $this->getRecord(); + $name = $this->getName(); + if($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) { + return 1; // Default for has_one + } else { + return null; // Default for has_many and many_many + } + } else { + return $allowedMaxFileNumber; + } + } + + /** + * Determine maximum number of files allowed to be attached. + * + * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited + * @return UploadField Self reference + */ + public function setAllowedMaxFileNumber($allowedMaxFileNumber) { + return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber); + } + + /** + * Determine if the user has permission to upload. + * + * @return boolean + */ + public function canUpload() { + if(!$this->isActive()) return false; + $can = $this->getConfig('canUpload'); + return (is_bool($can)) ? $can : Permission::check($can); + } + + /** + * Specify whether the user can upload files. + * String values will be treated as required permission codes + * + * @param boolean|string $canUpload Either a boolean flag, or a required + * permission code + * @return UploadField Self reference + */ + public function setCanUpload($canUpload) { + return $this->setConfig('canUpload', $canUpload); + } + + /** + * 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 + */ + public function canAttachExisting() { + if(!$this->isActive()) return false; + $can = $this->getConfig('canAttachExisting'); + return (is_bool($can)) ? $can : Permission::check($can); + } + + /** + * Returns true if the field is neither readonly nor disabled + * + * @return boolean + */ + public function isActive() { + return !$this->isDisabled() && !$this->isReadonly(); + } + + /** + * Specify whether the user can attach existing files + * String values will be treated as required permission codes + * + * @param boolean|string $canAttachExisting Either a boolean flag, or a + * required permission code + * @return UploadField Self reference + */ + public function setCanAttachExisting($canAttachExisting) { + return $this->setConfig('canAttachExisting', $canAttachExisting); + } + + /** + * Gets thumbnail width. Defaults to 80 + * + * @return integer + */ + public function getPreviewMaxWidth() { + return $this->getConfig('previewMaxWidth'); + } + + /** + * @see UploadField::getPreviewMaxWidth() + * + * @param integer $previewMaxWidth + * @return UploadField Self reference + */ + public function setPreviewMaxWidth($previewMaxWidth) { + return $this->setConfig('previewMaxWidth', $previewMaxWidth); + } + + /** + * Gets thumbnail height. Defaults to 60 + * + * @return integer + */ + public function getPreviewMaxHeight() { + return $this->getConfig('previewMaxHeight'); + } + + /** + * @see UploadField::getPreviewMaxHeight() + * + * @param integer $previewMaxHeight + * @return UploadField Self reference + */ + public function setPreviewMaxHeight($previewMaxHeight) { + return $this->setConfig('previewMaxHeight', $previewMaxHeight); + } + + /** + * javascript template used to display uploading files + * Defaults to 'ss-uploadfield-uploadtemplate' + * + * @see javascript/UploadField_uploadtemplate.js + * @return string + */ + public function getUploadTemplateName() { + return $this->getConfig('uploadTemplateName'); + } + + /** + * @see UploadField::getUploadTemplateName() + * + * @param string $uploadTemplateName + * @return UploadField Self reference + */ + public function setUploadTemplateName($uploadTemplateName) { + return $this->setConfig('uploadTemplateName', $uploadTemplateName); + } + + /** + * javascript template used to display already uploaded files + * Defaults to 'ss-downloadfield-downloadtemplate' + * + * @see javascript/DownloadField_downloadtemplate.js + * @return string + */ + public function getDownloadTemplateName() { + return $this->getConfig('downloadTemplateName'); + } + + /** + * @see Uploadfield::getDownloadTemplateName() + * + * @param string $downloadTemplateName + * @return Uploadfield Self reference + */ + public function setDownloadTemplateName($downloadTemplateName) { + return $this->setConfig('downloadTemplateName', $downloadTemplateName); + } + + /** + * FieldList $fields for the EditForm + * @example 'getCMSFields' + * + * @param DataObject $file File context to generate fields for + * @return FieldList List of form fields + */ + public function getFileEditFields(DataObject $file) { + // Empty actions, generate default + if(empty($this->fileEditFields)) { + $fields = $file->getCMSFields(); + // Only display main tab, to avoid overly complex interface + if($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) { + $fields = $mainTab->Fields(); + } + return $fields; + } + + // Fields instance + if ($this->fileEditFields instanceof FieldList) { + return $this->fileEditFields; + } + + // Method to call on the given file + if($file->hasMethod($this->fileEditFields)) { + return $file->{$this->fileEditFields}(); + } + + throw new InvalidArgumentException("Invalid value for UploadField::fileEditFields"); + } + + /** + * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm + * @example 'getCMSFields' + * + * @param FieldList|string + * @return Uploadfield Self reference + */ + public function setFileEditFields($fileEditFields) { + $this->fileEditFields = $fileEditFields; + return $this; + } + + /** + * 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 form actions for + * @return FieldList Field list containing FormAction + */ + public function getFileEditActions(DataObject $file) { + // Empty actions, generate default + if(empty($this->fileEditActions)) { + $actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save'))); + $saveAction->addExtraClass('ss-ui-action-constructive icon-accept'); + return $actions; + } + + // Actions instance + if ($this->fileEditActions instanceof FieldList) { + return $this->fileEditActions; + } + + // Method to call on the given file + if($file->hasMethod($this->fileEditActions)) { + return $file->{$this->fileEditActions}(); + } + + throw new InvalidArgumentException("Invalid value for UploadField::fileEditActions"); + } + + /** + * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm + * @example 'getCMSActions' + * + * @param FieldList|string + * @return Uploadfield Self reference + */ + public function setFileEditActions($fileEditActions) { + $this->fileEditActions = $fileEditActions; + return $this; + } + + /** + * Determines the validator to use for the edit form + * @example 'getCMSValidator' + * + * @param DataObject $file File context to generate validator from + * @return Validator Validator object + */ + public function getFileEditValidator(DataObject $file) { + // Empty validator + if(empty($this->fileEditValidator)) { + return null; + } + + // Validator instance + if($this->fileEditValidator instanceof Validator) { + return $this->fileEditValidator; + } + + // Method to call on the given file + if($file->hasMethod($this->fileEditValidator)) { + return $file->{$this->fileEditValidator}(); + } + + throw new InvalidArgumentException("Invalid value for UploadField::fileEditValidator"); + } + + /** + * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm + * @example 'getCMSValidator' + * + * @param Validator|string + * @return Uploadfield Self reference + */ + public function setFileEditValidator($fileEditValidator) { + $this->fileEditValidator = $fileEditValidator; + return $this; + } + + /** + * + * @param File|AssetContainer $file + * @return string URL to thumbnail + */ + protected function getThumbnailURLForFile(AssetContainer $file) { + if (!$file->exists()) { + return null; + } + + // Attempt to generate image at given size + $width = $this->getPreviewMaxWidth(); + $height = $this->getPreviewMaxHeight(); + if ($file->hasMethod('ThumbnailURL')) { + return $file->ThumbnailURL($width, $height); + } + if ($file->hasMethod('Thumbnail')) { + return $file->Thumbnail($width, $height)->getURL(); + } + if ($file->hasMethod('Fit')) { + return $file->Fit($width, $height)->getURL(); + } + + // Check if unsized icon is available + if($file->hasMethod('getIcon')) { + return $file->getIcon(); + } + return null; + } + + public function getAttributes() { + return array_merge( + parent::getAttributes(), + array( + 'type' => 'file', + 'data-selectdialog-url' => $this->Link('select') + ) + ); + } + + public function extraClass() { + if($this->isDisabled()) { + $this->addExtraClass('disabled'); + } + if($this->isReadonly()) { + $this->addExtraClass('readonly'); + } + + return parent::extraClass(); + } + + public function Field($properties = array()) { + // Calculated config as per jquery.fileupload-ui.js + $allowedMaxFileNumber = $this->getAllowedMaxFileNumber(); + $config = array( + 'url' => $this->Link('upload'), + 'urlSelectDialog' => $this->Link('select'), + 'urlAttach' => $this->Link('attach'), + 'urlFileExists' => $this->Link('fileexists'), + 'acceptFileTypes' => '.+$', + // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed + 'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null, + 'replaceFile' => $this->getUpload()->getReplaceFile(), + ); + + // Validation: File extensions + if ($allowedExtensions = $this->getAllowedExtensions()) { + $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; + $config['errorMessages']['acceptFileTypes'] = _t( + 'File.INVALIDEXTENSIONSHORT', + 'Extension is not allowed' + ); + } + + // Validation: File size + if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) { + $config['maxFileSize'] = $allowedMaxFileSize; + $config['errorMessages']['maxFileSize'] = _t( + 'File.TOOLARGESHORT', + 'File size exceeds {size}', + array('size' => File::format_size($config['maxFileSize'])) + ); + } + + // Validation: Number of files + if ($allowedMaxFileNumber) { + if($allowedMaxFileNumber > 1) { + $config['errorMessages']['maxNumberOfFiles'] = _t( + 'UploadField.MAXNUMBEROFFILESSHORT', + 'Can only upload {count} files', + array('count' => $allowedMaxFileNumber) + ); + } else { + $config['errorMessages']['maxNumberOfFiles'] = _t( + 'UploadField.MAXNUMBEROFFILESONE', + 'Can only upload one file' + ); + } + } + + // add overwrite warning error message to the config object sent to Javascript + if ($this->getOverwriteWarning()) { + $config['errorMessages']['overwriteWarning'] = + _t('UploadField.OVERWRITEWARNING', 'File with the same name already exists'); + } + + $mergedConfig = array_merge($config, $this->ufConfig); + return parent::Field(array( + 'configString' => Convert::raw2json($mergedConfig), + 'config' => new ArrayData($mergedConfig), + 'multiple' => $allowedMaxFileNumber !== 1 + )); + } + + /** + * Validation method for this field, called when the entire form is validated + * + * @param Validator $validator + * @return boolean + */ + public function validate($validator) { + $name = $this->getName(); + $files = $this->getItems(); + + // If there are no files then quit + if($files->count() == 0) return true; + + // Check max number of files + $maxFiles = $this->getAllowedMaxFileNumber(); + if($maxFiles && ($files->count() > $maxFiles)) { + $validator->validationError( + $name, + _t( + 'UploadField.MAXNUMBEROFFILES', + 'Max number of {count} file(s) exceeded.', + array('count' => $maxFiles) + ), + "validation" + ); + return false; + } + + // Revalidate each file against nested validator + $this->upload->clearErrors(); + foreach($files as $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, + 'tmp_name' => null, // Should bypass is_uploaded_file check + 'error' => UPLOAD_ERR_OK, + ); + $this->upload->validate($tmpFile); + } + + // Check all errors + if($errors = $this->upload->getErrors()) { + foreach($errors as $error) { + $validator->validationError($name, $error, "validation"); + } + return false; + } + + return true; + } + + /** + * @param HTTPRequest $request + * @return UploadField_ItemHandler + */ + public function handleItem(HTTPRequest $request) { + return $this->getItemHandler($request->param('ID')); + } + + /** + * @param int $itemID + * @return UploadField_ItemHandler + */ + public function getItemHandler($itemID) { + return UploadField_ItemHandler::create($this, $itemID); + } + + /** + * @param HTTPRequest $request + * @return UploadField_SelectHandler + */ + public function handleSelect(HTTPRequest $request) { + if(!$this->canAttachExisting()) { + return $this->httpError(403); + } + return UploadField_SelectHandler::create($this, $this->getFolderName()); + } + + /** + * Safely encodes the File object with all standard fields required + * by the front end + * + * @param File|AssetContainer $file Object which contains a file + * @return array Array encoded list of file attributes + */ + protected function encodeFileAttributes(AssetContainer $file) { + // Collect all output data. + $customised = $this->customiseFile($file); + return array( + 'id' => $file->ID, + 'name' => basename($file->getFilename()), + 'url' => $file->getURL(), + 'thumbnail_url' => $customised->UploadFieldThumbnailURL, + 'edit_url' => $customised->UploadFieldEditLink, + 'size' => $file->getAbsoluteSize(), + 'type' => File::get_file_type($file->getFilename()), + 'buttons' => (string)$customised->UploadFieldFileButtons, + 'fieldname' => $this->getName() + ); + } + + /** + * Action to handle upload of a single file + * + * @param HTTPRequest $request + * @return HTTPResponse + * @return HTTPResponse + */ + public function upload(HTTPRequest $request) { + if($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { + return $this->httpError(403); + } + + // Protect against CSRF on destructive action + $token = $this->getForm()->getSecurityToken(); + if(!$token->checkRequest($request)) return $this->httpError(400); + + // Get form details + $name = $this->getName(); + $postVars = $request->postVar($name); + + // Extract uploaded files from Form data + $uploadedFiles = $this->extractUploadedFileData($postVars); + $return = array(); + + // Save the temporary files into a File objects + // and save data/error on a per file basis + foreach ($uploadedFiles as $tempFile) { + $file = $this->saveTemporaryFile($tempFile, $error); + if(empty($file)) { + array_push($return, array('error' => $error)); + } else { + array_push($return, $this->encodeFileAttributes($file)); + } + $this->upload->clearErrors(); + } + + // Format response with json + $response = new HTTPResponse(Convert::raw2json($return)); + $response->addHeader('Content-Type', 'text/plain'); + return $response; + } + + /** + * Retrieves details for files that this field wishes to attache to the + * client-side form + * + * @param HTTPRequest $request + * @return HTTPResponse + */ + public function attach(HTTPRequest $request) { + if(!$request->isPOST()) return $this->httpError(403); + if(!$this->canAttachExisting()) return $this->httpError(403); + + // 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; + } - // 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')); - $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 $template - * @return $this - */ - public function setTemplateFileButtons($template) - { - $this->templateFileButtons = $template; - return $this; - } - - /** - * @return string - */ - 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 - * @return $this - */ - public function setTemplateFileEdit($template) - { - $this->templateFileEdit = $template; - return $this; - } - - /** - * @return string - */ - public function getTemplateFileEdit() - { - return $this->_templates($this->templateFileEdit, '_FileEdit'); - } - - /** - * Determine if the target folder for new uploads in is visible the field UI. - * - * @return boolean - */ - public function canPreviewFolder() - { - if (!$this->isActive()) { - return false; - } - $can = $this->getConfig('canPreviewFolder'); - return (is_bool($can)) ? $can : Permission::check($can); - } - - /** - * Determine if the target folder for new uploads in is visible the field UI. - * Disable to keep the internal filesystem structure hidden from users. - * - * @param boolean|string $canPreviewFolder Either a boolean flag, or a - * required permission code - * @return UploadField Self reference - */ - public function setCanPreviewFolder($canPreviewFolder) - { - return $this->setConfig('canPreviewFolder', $canPreviewFolder); - } - - /** - * 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) - * - * @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) - * - * @param boolean $overwriteWarning - * @return UploadField Self reference - */ - public function setOverwriteWarning($overwriteWarning) - { - return $this->setConfig('overwriteWarning', $overwriteWarning); - } - - /** - * @param string $name - * @return $this - */ - public function setDisplayFolderName($name) - { - $this->displayFolderName = $name; - return $this; - } - - /** - * @return String - */ - public function getDisplayFolderName() - { - return $this->displayFolderName; - } - - /** - * Force a record to be used as "Parent" for uploaded Files (eg a Page with a has_one to File) - * - * @param DataObject $record - * @return $this - */ - public function setRecord($record) - { - $this->record = $record; - return $this; - } - /** - * Get the record to use as "Parent" for uploaded Files (eg a Page with a has_one to File) If none is set, it will - * use Form->getRecord() or Form->Controller()->data() - * - * @return DataObject - */ - public function getRecord() - { - if (!$this->record && $this->form) { - if (($record = $this->form->getRecord()) && ($record instanceof DataObject)) { - $this->record = $record; - } elseif (($controller = $this->form->getController()) - && $controller->hasMethod('data') - && ($record = $controller->data()) - && ($record instanceof DataObject) - ) { - $this->record = $record; - } - } - return $this->record; - } - - /** - * Loads the related record values into this field. UploadField can be uploaded - * in one of three ways: - * - * - 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). - * - By passing in an explicit list of File objects in the $record parameter, and - * leaving $value blank. - * - By passing in a dataobject in the $record parameter, from which file objects - * 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 - * field value (list of file ID values). - * - * @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, - * SS_List of items, or an array of submitted form data - * @return $this Self reference - * @throws ValidationException - */ - public function setValue($value, $record = null) - { - - // If we're not passed a value directly, we can attempt to infer the field - // value from the second parameter by inspecting its relations - $items = new ArrayList(); - - // Determine format of presented data - if (empty($value) && $record) { - // If a record is given as a second parameter, but no submitted values, - // then we should inspect this instead for the form values - - if (($record instanceof DataObject) && $record->hasMethod($this->getName())) { - // If given a dataobject use reflection to extract details - - $data = $record->{$this->getName()}(); - if ($data instanceof DataObject) { - // If has_one, add sole item to default list - $items->push($data); - } elseif ($data instanceof SS_List) { - // For many_many and has_many relations we can use the relation list directly - $items = $data; - } - } elseif ($record instanceof SS_List) { - // If directly passing a list then save the items directly - $items = $record; - } - } elseif (!empty($value['Files'])) { - // If value is given as an array (such as a posted form), extract File IDs from this - $class = $this->getRelationAutosetClass(); - $items = DataObject::get($class)->byIDs($value['Files']); - } - - // If javascript is disabled, direct file upload (non-html5 style) can - // trigger a single or multiple file submission. Note that this may be - // included in addition to re-submitted File IDs as above, so these - // should be added to the list instead of operated on independently. - if ($uploadedFiles = $this->extractUploadedFileData($value)) { - foreach ($uploadedFiles as $tempFile) { - $file = $this->saveTemporaryFile($tempFile, $error); - if ($file) { - $items->add($file); - } else { - throw new ValidationException($error); - } - } - } - - // Filter items by what's allowed to be viewed - $filteredItems = new ArrayList(); - $fileIDs = array(); - foreach ($items as $file) { - if ($file->exists() && $file->canView()) { - $filteredItems->push($file); - $fileIDs[] = $file->ID; - } - } - - // Filter and cache updated item list - $this->items = $filteredItems; - // Same format as posted form values for this field. Also ensures that - // $this->setValue($this->getValue()); is non-destructive - $value = $fileIDs ? array('Files' => $fileIDs) : null; - - // Set value using parent - parent::setValue($value, $record); - return $this; - } - - /** - * 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 - * updating the internal list of File items. - * - * @param SS_List $items - * @return UploadField self reference - */ - public function setItems(SS_List $items) - { - return $this->setValue(null, $items); - } - - /** - * Retrieves the current list of files - * - * @return SS_List - */ - public function getItems() - { - return $this->items ? $this->items : new ArrayList(); - } - - /** - * Retrieves a customised list of all File records to ensure they are - * properly viewable when rendered in the field template. - * - * @return SS_List[ViewableData_Customised] - */ - public function getCustomisedItems() - { - $customised = new ArrayList(); - foreach ($this->getItems() as $file) { - $customised->push($this->customiseFile($file)); - } - return $customised; - } - - /** - * Retrieves the list of selected file IDs - * - * @return array - */ - public function getItemIDs() - { - $value = $this->Value(); - return empty($value['Files']) ? array() : $value['Files']; - } - - public function Value() - { - // Re-override FileField Value to use data value - return $this->dataValue(); - } - - /** - * @param DataObject|DataObjectInterface $record - * @return $this - */ - public function saveInto(DataObjectInterface $record) - { - // Check required relation details are available - $fieldname = $this->getName(); - if (!$fieldname) { - return $this; - } - - // Get details to save - $idList = $this->getItemIDs(); - - // Check type of relation - $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; - if ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) { - // has_many or many_many - $relation->setByIDList($idList); - } elseif (DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) { - // has_one - $record->{"{$fieldname}ID"} = $idList ? reset($idList) : 0; - } - return $this; - } - - /** - * Customises a file with additional details suitable for rendering in the - * UploadField.ss template - * - * @param ViewableData|AssetContainer $file - * @return ViewableData_Customised - */ - protected function customiseFile(AssetContainer $file) - { - $file = $file->customise(array( - 'UploadFieldThumbnailURL' => $this->getThumbnailURLForFile($file), - 'UploadFieldDeleteLink' => $this->getItemHandler($file->ID)->DeleteLink(), - 'UploadFieldEditLink' => $this->getItemHandler($file->ID)->EditLink(), - 'UploadField' => $this - )); - // we do this in a second customise to have the access to the previous customisations - return $file->customise(array( - 'UploadFieldFileButtons' => $file->renderWith($this->getTemplateFileButtons()) - )); - } - - /** - * Assign a front-end config variable for the upload field - * - * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available - * - * @param string $key - * @param mixed $val - * @return UploadField self reference - */ - public function setConfig($key, $val) - { - $this->ufConfig[$key] = $val; - return $this; - } - - /** - * Gets a front-end config variable for the upload field - * - * @see https://github.com/blueimp/jQuery-File-Upload/wiki/Options for the list of front end options available - * - * @param string $key - * @return mixed - */ - public function getConfig($key) - { - if (!isset($this->ufConfig[$key])) { - return null; - } - return $this->ufConfig[$key]; - } - - /** - * Determine if the field should automatically upload the file. - * - * @return boolean - */ - public function getAutoUpload() - { - return $this->getConfig('autoUpload'); - } - - /** - * Determine if the field should automatically upload the file - * - * @param boolean $autoUpload - * @return UploadField Self reference - */ - public function setAutoUpload($autoUpload) - { - return $this->setConfig('autoUpload', $autoUpload); - } - - /** - * 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 - */ - public function getAllowedMaxFileNumber() - { - $allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber'); - - // if there is a has_one relation with that name on the record and - // allowedMaxFileNumber has not been set, it's wanted to be 1 - if (empty($allowedMaxFileNumber)) { - $record = $this->getRecord(); - $name = $this->getName(); - if ($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) { - return 1; // Default for has_one - } else { - return null; // Default for has_many and many_many - } - } else { - return $allowedMaxFileNumber; - } - } - - /** - * Determine maximum number of files allowed to be attached. - * - * @param integer|null $allowedMaxFileNumber Maximum limit. 0 or null will be treated as unlimited - * @return UploadField Self reference - */ - public function setAllowedMaxFileNumber($allowedMaxFileNumber) - { - return $this->setConfig('allowedMaxFileNumber', $allowedMaxFileNumber); - } - - /** - * Determine if the user has permission to upload. - * - * @return boolean - */ - public function canUpload() - { - if (!$this->isActive()) { - return false; - } - $can = $this->getConfig('canUpload'); - return (is_bool($can)) ? $can : Permission::check($can); - } - - /** - * Specify whether the user can upload files. - * String values will be treated as required permission codes - * - * @param boolean|string $canUpload Either a boolean flag, or a required - * permission code - * @return UploadField Self reference - */ - public function setCanUpload($canUpload) - { - return $this->setConfig('canUpload', $canUpload); - } - - /** - * 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 - */ - public function canAttachExisting() - { - if (!$this->isActive()) { - return false; - } - $can = $this->getConfig('canAttachExisting'); - return (is_bool($can)) ? $can : Permission::check($can); - } - - /** - * Returns true if the field is neither readonly nor disabled - * - * @return boolean - */ - public function isActive() - { - return !$this->isDisabled() && !$this->isReadonly(); - } - - /** - * Specify whether the user can attach existing files - * String values will be treated as required permission codes - * - * @param boolean|string $canAttachExisting Either a boolean flag, or a - * required permission code - * @return UploadField Self reference - */ - public function setCanAttachExisting($canAttachExisting) - { - return $this->setConfig('canAttachExisting', $canAttachExisting); - } - - /** - * Gets thumbnail width. Defaults to 80 - * - * @return integer - */ - public function getPreviewMaxWidth() - { - return $this->getConfig('previewMaxWidth'); - } - - /** - * @see UploadField::getPreviewMaxWidth() - * - * @param integer $previewMaxWidth - * @return UploadField Self reference - */ - public function setPreviewMaxWidth($previewMaxWidth) - { - return $this->setConfig('previewMaxWidth', $previewMaxWidth); - } - - /** - * Gets thumbnail height. Defaults to 60 - * - * @return integer - */ - public function getPreviewMaxHeight() - { - return $this->getConfig('previewMaxHeight'); - } - - /** - * @see UploadField::getPreviewMaxHeight() - * - * @param integer $previewMaxHeight - * @return UploadField Self reference - */ - public function setPreviewMaxHeight($previewMaxHeight) - { - return $this->setConfig('previewMaxHeight', $previewMaxHeight); - } - - /** - * javascript template used to display uploading files - * Defaults to 'ss-uploadfield-uploadtemplate' - * - * @see javascript/UploadField_uploadtemplate.js - * @return string - */ - public function getUploadTemplateName() - { - return $this->getConfig('uploadTemplateName'); - } - - /** - * @see UploadField::getUploadTemplateName() - * - * @param string $uploadTemplateName - * @return UploadField Self reference - */ - public function setUploadTemplateName($uploadTemplateName) - { - return $this->setConfig('uploadTemplateName', $uploadTemplateName); - } - - /** - * javascript template used to display already uploaded files - * Defaults to 'ss-downloadfield-downloadtemplate' - * - * @see javascript/DownloadField_downloadtemplate.js - * @return string - */ - public function getDownloadTemplateName() - { - return $this->getConfig('downloadTemplateName'); - } - - /** - * @see Uploadfield::getDownloadTemplateName() - * - * @param string $downloadTemplateName - * @return Uploadfield Self reference - */ - public function setDownloadTemplateName($downloadTemplateName) - { - return $this->setConfig('downloadTemplateName', $downloadTemplateName); - } - - /** - * FieldList $fields for the EditForm - * @example 'getCMSFields' - * - * @param DataObject $file File context to generate fields for - * @return FieldList List of form fields - */ - public function getFileEditFields(DataObject $file) - { - // Empty actions, generate default - if (empty($this->fileEditFields)) { - $fields = $file->getCMSFields(); - // Only display main tab, to avoid overly complex interface - if ($fields->hasTabSet() && ($mainTab = $fields->findOrMakeTab('Root.Main'))) { - $fields = $mainTab->Fields(); - } - return $fields; - } - - // Fields instance - if ($this->fileEditFields instanceof FieldList) { - return $this->fileEditFields; - } - - // Method to call on the given file - if ($file->hasMethod($this->fileEditFields)) { - return $file->{$this->fileEditFields}(); - } - - throw new InvalidArgumentException("Invalid value for UploadField::fileEditFields"); - } - - /** - * FieldList $fields or string $name (of a method on File to provide a fields) for the EditForm - * @example 'getCMSFields' - * - * @param FieldList|string - * @return Uploadfield Self reference - */ - public function setFileEditFields($fileEditFields) - { - $this->fileEditFields = $fileEditFields; - return $this; - } - - /** - * 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 form actions for - * @return FieldList Field list containing FormAction - */ - public function getFileEditActions(DataObject $file) - { - // Empty actions, generate default - if (empty($this->fileEditActions)) { - $actions = new FieldList($saveAction = new FormAction('doEdit', _t('UploadField.DOEDIT', 'Save'))); - $saveAction->addExtraClass('ss-ui-action-constructive icon-accept'); - return $actions; - } - - // Actions instance - if ($this->fileEditActions instanceof FieldList) { - return $this->fileEditActions; - } - - // Method to call on the given file - if ($file->hasMethod($this->fileEditActions)) { - return $file->{$this->fileEditActions}(); - } - - throw new InvalidArgumentException("Invalid value for UploadField::fileEditActions"); - } - - /** - * FieldList $actions or string $name (of a method on File to provide a actions) for the EditForm - * @example 'getCMSActions' - * - * @param FieldList|string - * @return Uploadfield Self reference - */ - public function setFileEditActions($fileEditActions) - { - $this->fileEditActions = $fileEditActions; - return $this; - } - - /** - * Determines the validator to use for the edit form - * @example 'getCMSValidator' - * - * @param DataObject $file File context to generate validator from - * @return Validator Validator object - */ - public function getFileEditValidator(DataObject $file) - { - // Empty validator - if (empty($this->fileEditValidator)) { - return null; - } - - // Validator instance - if ($this->fileEditValidator instanceof Validator) { - return $this->fileEditValidator; - } - - // Method to call on the given file - if ($file->hasMethod($this->fileEditValidator)) { - return $file->{$this->fileEditValidator}(); - } - - throw new InvalidArgumentException("Invalid value for UploadField::fileEditValidator"); - } - - /** - * Validator (eg RequiredFields) or string $name (of a method on File to provide a Validator) for the EditForm - * @example 'getCMSValidator' - * - * @param Validator|string - * @return Uploadfield Self reference - */ - public function setFileEditValidator($fileEditValidator) - { - $this->fileEditValidator = $fileEditValidator; - return $this; - } - - /** - * - * @param File|AssetContainer $file - * @return string URL to thumbnail - */ - protected function getThumbnailURLForFile(AssetContainer $file) - { - if (!$file->exists()) { - return null; - } - - // Attempt to generate image at given size - $width = $this->getPreviewMaxWidth(); - $height = $this->getPreviewMaxHeight(); - if ($file->hasMethod('ThumbnailURL')) { - return $file->ThumbnailURL($width, $height); - } - if ($file->hasMethod('Thumbnail')) { - return $file->Thumbnail($width, $height)->getURL(); - } - if ($file->hasMethod('Fit')) { - return $file->Fit($width, $height)->getURL(); - } - - // Check if unsized icon is available - if ($file->hasMethod('getIcon')) { - return $file->getIcon(); - } - return null; - } - - public function getAttributes() - { - return array_merge( - parent::getAttributes(), - array('data-selectdialog-url', $this->Link('select')) - ); - } - - public function extraClass() - { - if ($this->isDisabled()) { - $this->addExtraClass('disabled'); - } - if ($this->isReadonly()) { - $this->addExtraClass('readonly'); - } - - return parent::extraClass(); - } - - public function Field($properties = array()) - { - // Calculated config as per jquery.fileupload-ui.js - $allowedMaxFileNumber = $this->getAllowedMaxFileNumber(); - $config = array( - 'url' => $this->Link('upload'), - 'urlSelectDialog' => $this->Link('select'), - 'urlAttach' => $this->Link('attach'), - 'urlFileExists' => $this->Link('fileexists'), - 'acceptFileTypes' => '.+$', - // Fileupload treats maxNumberOfFiles as the max number of _additional_ items allowed - 'maxNumberOfFiles' => $allowedMaxFileNumber ? ($allowedMaxFileNumber - count($this->getItemIDs())) : null, - 'replaceFile' => $this->getUpload()->getReplaceFile(), - ); - - // Validation: File extensions - if ($allowedExtensions = $this->getAllowedExtensions()) { - $config['acceptFileTypes'] = '(\.|\/)(' . implode('|', $allowedExtensions) . ')$'; - $config['errorMessages']['acceptFileTypes'] = _t( - 'File.INVALIDEXTENSIONSHORT', - 'Extension is not allowed' - ); - } - - // Validation: File size - if ($allowedMaxFileSize = $this->getValidator()->getAllowedMaxFileSize()) { - $config['maxFileSize'] = $allowedMaxFileSize; - $config['errorMessages']['maxFileSize'] = _t( - 'File.TOOLARGESHORT', - 'File size exceeds {size}', - array('size' => File::format_size($config['maxFileSize'])) - ); - } - - // Validation: Number of files - if ($allowedMaxFileNumber) { - if ($allowedMaxFileNumber > 1) { - $config['errorMessages']['maxNumberOfFiles'] = _t( - 'UploadField.MAXNUMBEROFFILESSHORT', - 'Can only upload {count} files', - array('count' => $allowedMaxFileNumber) - ); - } else { - $config['errorMessages']['maxNumberOfFiles'] = _t( - 'UploadField.MAXNUMBEROFFILESONE', - 'Can only upload one file' - ); - } - } - - // add overwrite warning error message to the config object sent to Javascript - if ($this->getOverwriteWarning()) { - $config['errorMessages']['overwriteWarning'] = - _t('UploadField.OVERWRITEWARNING', 'File with the same name already exists'); - } - - $mergedConfig = array_merge($config, $this->ufConfig); - return parent::Field(array( - 'configString' => Convert::raw2json($mergedConfig), - 'config' => new ArrayData($mergedConfig), - 'multiple' => $allowedMaxFileNumber !== 1 - )); - } - - /** - * Validation method for this field, called when the entire form is validated - * - * @param Validator $validator - * @return boolean - */ - public function validate($validator) - { - $name = $this->getName(); - $files = $this->getItems(); - - // If there are no files then quit - if ($files->count() == 0) { - return true; - } - - // Check max number of files - $maxFiles = $this->getAllowedMaxFileNumber(); - if ($maxFiles && ($files->count() > $maxFiles)) { - $validator->validationError( - $name, - _t( - 'UploadField.MAXNUMBEROFFILES', - 'Max number of {count} file(s) exceeded.', - array('count' => $maxFiles) - ), - "validation" - ); - return false; - } - - // Revalidate each file against nested validator - $this->upload->clearErrors(); - foreach ($files as $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, - 'tmp_name' => null, // Should bypass is_uploaded_file check - 'error' => UPLOAD_ERR_OK, - ); - $this->upload->validate($tmpFile); - } - - // Check all errors - if ($errors = $this->upload->getErrors()) { - foreach ($errors as $error) { - $validator->validationError($name, $error, "validation"); - } - return false; - } - - return true; - } - - /** - * @param HTTPRequest $request - * @return UploadField_ItemHandler - */ - public function handleItem(HTTPRequest $request) - { - return $this->getItemHandler($request->param('ID')); - } - - /** - * @param int $itemID - * @return UploadField_ItemHandler - */ - public function getItemHandler($itemID) - { - return UploadField_ItemHandler::create($this, $itemID); - } - - /** - * @param HTTPRequest $request - * @return UploadField_SelectHandler - */ - public function handleSelect(HTTPRequest $request) - { - if (!$this->canAttachExisting()) { - return $this->httpError(403); - } - return UploadField_SelectHandler::create($this, $this->getFolderName()); - } - - /** - * Given an array of post variables, extract all temporary file data into an array - * - * @param array $postVars Array of posted form data - * @return array List of temporary file data - */ - protected function extractUploadedFileData($postVars) - { - - // Note: Format of posted file parameters in php is a feature of using - // for multiple file uploads - $tmpFiles = array(); - if (!empty($postVars['tmp_name']) - && is_array($postVars['tmp_name']) - && !empty($postVars['tmp_name']['Uploads']) - ) { - for ($i = 0; $i < count($postVars['tmp_name']['Uploads']); $i++) { - // Skip if "empty" file - if (empty($postVars['tmp_name']['Uploads'][$i])) { - continue; - } - $tmpFile = array(); - foreach (array('name', 'type', 'tmp_name', 'error', 'size') as $field) { - $tmpFile[$field] = $postVars[$field]['Uploads'][$i]; - } - $tmpFiles[] = $tmpFile; - } - } elseif (!empty($postVars['tmp_name'])) { - // Fallback to allow single file uploads (method used by AssetUploadField) - $tmpFiles[] = $postVars; - } - - return $tmpFiles; - } - - /** - * Loads the temporary file data into a File object - * - * @param array $tmpFile Temporary file data - * @param string $error Error message - * @return AssetContainer File object, or null if error - */ - protected function saveTemporaryFile($tmpFile, &$error = null) - { - // Determine container object - $error = null; - $fileObject = null; - - if (empty($tmpFile)) { - $error = _t('UploadField.FIELDNOTSET', 'File information not found'); - return null; - } - - if ($tmpFile['error']) { - $error = $tmpFile['error']; - return null; - } - - // Search for relations that can hold the uploaded files, but don't fallback - // to default if there is no automatic relation - if ($relationClass = $this->getRelationAutosetClass(null)) { - // Allow File to be subclassed - if ($relationClass === 'SilverStripe\\Assets\\File' && isset($tmpFile['name'])) { - $relationClass = File::get_class_for_file_extension( - File::get_file_extension($tmpFile['name']) - ); - } - // Create new object explicitly. Otherwise rely on Upload::load to choose the class. - $fileObject = Object::create($relationClass); - if (! ($fileObject instanceof DataObject) || !($fileObject instanceof AssetContainer)) { - throw new InvalidArgumentException("Invalid asset container $relationClass"); - } - } - - // Get the uploaded file into a new file object. - try { - $this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName()); - } catch (Exception $e) { - // we shouldn't get an error here, but just in case - $error = $e->getMessage(); - return null; - } - - // Check if upload field has an error - if ($this->upload->isError()) { - $error = implode(' ' . PHP_EOL, $this->upload->getErrors()); - return null; - } - - // return file - return $this->upload->getFile(); - } - - /** - * Safely encodes the File object with all standard fields required - * by the front end - * - * @param File|AssetContainer $file Object which contains a file - * @return array Array encoded list of file attributes - */ - protected function encodeFileAttributes(AssetContainer $file) - { - // Collect all output data. - $customised = $this->customiseFile($file); - return array( - 'id' => $file->ID, - 'name' => basename($file->getFilename()), - 'url' => $file->getURL(), - 'thumbnail_url' => $customised->UploadFieldThumbnailURL, - 'edit_url' => $customised->UploadFieldEditLink, - 'size' => $file->getAbsoluteSize(), - 'type' => File::get_file_type($file->getFilename()), - 'buttons' => (string)$customised->UploadFieldFileButtons, - 'fieldname' => $this->getName() - ); - } - - /** - * Action to handle upload of a single file - * - * @param HTTPRequest $request - * @return HTTPResponse - * @return HTTPResponse - */ - public function upload(HTTPRequest $request) - { - if ($this->isDisabled() || $this->isReadonly() || !$this->canUpload()) { - return $this->httpError(403); - } - - // Protect against CSRF on destructive action - $token = $this->getForm()->getSecurityToken(); - if (!$token->checkRequest($request)) { - return $this->httpError(400); - } - - // Get form details - $name = $this->getName(); - $postVars = $request->postVar($name); - - // Extract uploaded files from Form data - $uploadedFiles = $this->extractUploadedFileData($postVars); - $return = array(); - - // Save the temporary files into a File objects - // and save data/error on a per file basis - foreach ($uploadedFiles as $tempFile) { - $file = $this->saveTemporaryFile($tempFile, $error); - if (empty($file)) { - array_push($return, array('error' => $error)); - } else { - array_push($return, $this->encodeFileAttributes($file)); - } - $this->upload->clearErrors(); - } - - // Format response with json - $response = new HTTPResponse(Convert::raw2json($return)); - $response->addHeader('Content-Type', 'text/plain'); - return $response; - } - - /** - * Retrieves details for files that this field wishes to attache to the - * client-side form - * - * @param HTTPRequest $request - * @return HTTPResponse - */ - public function attach(HTTPRequest $request) - { - if (!$request->isPOST()) { - return $this->httpError(403); - } - if (!$this->canAttachExisting()) { - return $this->httpError(403); - } - - // 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; - } - - /** - * 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; - } - } } diff --git a/src/Forms/Uploadable.php b/src/Forms/Uploadable.php new file mode 100644 index 000000000..e6242831f --- /dev/null +++ b/src/Forms/Uploadable.php @@ -0,0 +1,153 @@ +setUpload(Upload::create()); + + // 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')); + $this->getValidator()->setAllowedMaxFileSize(min($maxUpload, $maxPost)); + } + + /** + * Retrieves the Upload handler + * + * @return Upload + */ + public function getUpload() { + return $this->upload; + } + + /** + * Sets the upload handler + * + * @param Upload $upload + * @return $this Self reference + */ + public function setUpload(Upload $upload) { + $this->upload = $upload; + return $this; + } + + /** + * Limit allowed file extensions. Empty by default, allowing all extensions. + * To allow files without an extension, use an empty string. + * See {@link File::$allowed_extensions} to get a good standard set of + * extensions that are typically not harmful in a webserver context. + * See {@link setAllowedMaxFileSize()} to limit file size by extension. + * + * @param array $rules List of extensions + * @return $this + */ + public function setAllowedExtensions($rules) { + $this->getValidator()->setAllowedExtensions($rules); + return $this; + } + + /** + * Limit allowed file extensions by specifying categories of file types. + * These may be 'image', 'image/supported', 'audio', 'video', 'archive', 'flash', or 'document' + * See {@link File::$allowed_extensions} for details of allowed extensions + * for each of these categories + * + * @param string $category Category name + * @param string,... $categories Additional category names + * @return $this + */ + public function setAllowedFileCategories($category) { + $extensions = File::get_category_extensions(func_get_args()); + return $this->setAllowedExtensions($extensions); + } + + /** + * Returns list of extensions allowed by this field, or an empty array + * if there is no restriction + * + * @return array + */ + public function getAllowedExtensions() { + return $this->getValidator()->getAllowedExtensions(); + } + + /** + * Get custom validator for this field + * + * @return Upload_Validator + */ + public function getValidator() { + return $this->getUpload()->getValidator(); + } + + /** + * Set custom validator for this field + * + * @param Upload_Validator $validator + * @return $this Self reference + */ + public function setValidator(Upload_Validator $validator) { + $this->getUpload()->setValidator($validator); + return $this; + } + + /** + * Sets the upload folder name + * + * @param string $folderName + * @return $this Self reference + */ + public function setFolderName($folderName) { + $this->folderName = $folderName; + return $this; + } + + /** + * Gets the upload folder name + * + * @return string + */ + public function getFolderName() { + return ($this->folderName !== false) + ? $this->folderName + : Upload::config()->uploads_folder; + } +}