diff --git a/composer.json b/composer.json index 3f03ab8..72a895b 100644 --- a/composer.json +++ b/composer.json @@ -4,24 +4,30 @@ "type": "silverstripe-vendormodule", "keywords": ["silverstripe", "upload", "uploader", "files", "forms", "cms"], "license": "BSD-3-Clause", - "authors": [{ - "name": "Uncle Cheese", - "email": "unclecheese@leftandmain.com" - }], - "require": { + "authors": [ + { + "name": "Uncle Cheese", + "email": "unclecheese@leftandmain.com" + } + ], + "require": + { "silverstripe/framework": "4.*" }, + "autoload": { + "psr-4": { + "UncleCheese\\DropZone\\": "code/" + } + }, + "minimum-stability": "dev", + "prefer-stable": true, "extra": { "installer-name": "dropzone", "expose": [ - "javascript", "css", - "images" - ] - }, - "autoload": { - "psr-4": { - "UncleCheese\\Dropzone\\": "src/" - } - } -} \ No newline at end of file + "javascript", + "images", + "templates" + ] + } +} diff --git a/src/DropzoneFile.php b/src/DropzoneFile.php index 8eb98fd..a40c0aa 100644 --- a/src/DropzoneFile.php +++ b/src/DropzoneFile.php @@ -1,18 +1,17 @@ */ class DropzoneFile extends DataExtension @@ -22,7 +21,7 @@ class DropzoneFile extends DataExtension /** * Helper method for determining if this is an Image * - * @return boolean + * @return boolean */ public function IsImage() { @@ -34,41 +33,38 @@ public function IsImage() * Gets a thumbnail for this file given a size. If it's an Image, * it will render the actual file. If not, it will provide an icon based * on the extension. - * * @param int $w The width of the image * @param int $h The height of the image * @return Image_Cached */ public function getPreviewThumbnail($w = null, $h = null) { - if(!$w) { $w = $this->owner->config()->grid_thumbnail_width; + if (!$w) { + $w = $this->owner->config()->grid_thumbnail_width; } - if(!$h) { $h = $this->owner->config()->grid_thumbnail_height; + if (!$h) { + $h = $this->owner->config()->grid_thumbnail_height; } - if($this->IsImage() && Director::fileExists($this->owner->Filename)) { + if ($this->IsImage() && Director::fileExists($this->owner->Filename)) { return $this->owner->CroppedImage($w, $h); } - $sizes = Config::inst()->forClass(FileAttachmentField::class)->icon_sizes; + $sizes = Config::inst()->forClass('FileAttachmentField')->icon_sizes; sort($sizes); - foreach($sizes as $size) { - if($w <= $size) { - if($this->owner instanceof Folder) { + foreach ($sizes as $size) { + if ($w <= $size) { + if ($this->owner instanceof Folder) { $file = $this->getFilenameForType('_folder', $size); - } - else { + } else { $file = $this->getFilenameForType($this->owner->getExtension(), $size); } - if(!file_exists(BASE_PATH.'/'.$file)) { + if (!file_exists(BASE_PATH . '/' . $file)) { $file = $this->getFilenameForType('_blank', $size); } - $image = Image::create(); - $image->setFromLocalFile(Director::getAbsFile($file), basename($file)); - - return $image; + return new File(Director::makeRelative($file)); } } } @@ -77,16 +73,17 @@ public function getPreviewThumbnail($w = null, $h = null) /** * Gets a filename based on the extension and the size * - * @param string $ext The extension of the file, e.g. "pdf" - * @param int $size The size of the image + * @param string $ext The extension of the file, e.g. "pdf" + * @param int $size The size of the image * @return string */ protected function getFilenameForType($ext, $size) { - return ModuleResourceLoader::singleton()->resolveURL(sprintf( - 'unclecheese/dropzone:images/file-icons/%spx/%s.png', + return sprintf( + '%s/images/file-icons/%spx/%s.png', + basename(dirname(__FILE__)), $size, strtolower($ext) - )); + ); } -} \ No newline at end of file +} diff --git a/src/FileAttachmentField.php b/src/FileAttachmentField.php index ddea293..9dd9b47 100644 --- a/src/FileAttachmentField.php +++ b/src/FileAttachmentField.php @@ -1,34 +1,44 @@ */ class FileAttachmentField extends FileField @@ -36,18 +46,17 @@ class FileAttachmentField extends FileField /** * The allowed actions for the RequestHandler - * * @var array */ - private static $allowed_actions = array ( + private static $allowed_actions = [ 'upload', 'handleSelect', - ); + ]; - private static $url_handlers = array ( + private static $url_handlers = [ 'select' => 'handleSelect', - ); + ]; /** * Track files that are uploaded and remove the tracked files when @@ -59,21 +68,18 @@ class FileAttachmentField extends FileField /** * A list of settings for this instance - * * @var array */ protected $settings = []; /** * Extra params to send to the server with the POST request - * * @var array */ protected $params = []; /** * The record that this FormField is editing - * * @var DataObject */ protected $record = null; @@ -85,14 +91,12 @@ class FileAttachmentField extends FileField * - attach (select from existing) * - detach (remove from record, but don't delete) * -delete (delete from files) - * * @var array */ protected $permissions = []; /** * The style of uploader. Options: "grid", "list" - * * @var string */ protected $view = 'list'; @@ -101,44 +105,37 @@ class FileAttachmentField extends FileField * The preview template for uploaded files. Does not necessarily apply * to files that were on the record at load time, but rather to files * that have been attached to the uploader client side - * * @var string */ - protected $previewTemplate = 'UncleCheese\\Dropzone\\FileAttachmentField_preview'; + protected $previewTemplate = 'UncleCheese\DropZone\Includes\FileAttachmentField_preview'; /** * UploadField compatability. Used for the select handler, when KickAssets * is not installed - * * @var string */ protected $displayFolderName; /** * Set to true if detected invalid file ID - * * @var boolean */ protected $hasInvalidFileID; /** * Helper function to translate underscore_case to camelCase - * * @param string $str * @return string */ public static function camelise($str) { - return preg_replace_callback( - '/_([a-z])/', function ($c) { - return strtoupper($c[1]); - }, $str - ); + return preg_replace_callback('/_([a-z])/', function ($c) { + return strtoupper($c[1]); + }, $str); } /** * Translate camelCase to underscore_case - * * @param string $str * @return string */ @@ -146,11 +143,9 @@ public static function underscorise($str) { $str[0] = strtolower($str[0]); - return preg_replace_callback( - '/([A-Z])/', function ($c) { - return "_" . strtolower($c[1]); - }, $str - ); + return preg_replace_callback('/([A-Z])/', function ($c) { + return "_" . strtolower($c[1]); + }, $str); } /** @@ -161,23 +156,20 @@ public static function underscorise($str) */ public static function get_filesize_from_ini() { - $bytes = min( - array( + $bytes = min([ File::ini2bytes(ini_get('post_max_size') ?: '8M'), File::ini2bytes(ini_get('upload_max_filesize') ?: '2M') - ) - ); + ]); - return floor($bytes/(1024*1024)); + return floor($bytes / (1024 * 1024)); } /** * Constructor. Sets some default permissions - * * @param string $name * @param string $title * @param string $value - * @param Form $form + * @param Form $form */ public function __construct($name, $title = null, $value = null, $form = null) { @@ -192,9 +184,6 @@ public function __construct($name, $title = null, $value = null, $form = null) return $instance->isCMS(); }; - $this->setFieldHolderTemplate(__NAMESPACE__ . '\\FileAttachmentField_holder'); - $this->setSmallFieldHolderTemplate(__NAMESPACE__ . '\\FileAttachmentField_holder_small'); - parent::__construct($name, $title, $value, $form); } @@ -202,10 +191,10 @@ public function __construct($name, $title = null, $value = null, $form = null) * Renders the form field, loads requirements. Sets file size based on php.ini * Adds the security token * - * @param array $attributes - * @return SSViewer + * @param array $attributes + * @return SSViewer */ - public function FieldHolder($attributes = array ()) + public function FieldHolder($attributes = []) { $this->defineFieldHolderRequirements(); return parent::FieldHolder($attributes); @@ -215,10 +204,10 @@ public function FieldHolder($attributes = array ()) * Renders the small form field holder, loads requirements. Sets file size based on php.ini * Adds the security token * - * @param array $attributes - * @return SSViewer + * @param array $attributes + * @return SSViewer */ - public function SmallFieldHolder($attributes = array ()) + public function SmallFieldHolder($attributes = []) { $this->defineFieldHolderRequirements(); return parent::SmallFieldHolder($attributes); @@ -229,18 +218,18 @@ public function SmallFieldHolder($attributes = array ()) */ protected function defineFieldHolderRequirements() { - Requirements::javascript('unclecheese/dropzone:javascript/dropzone.js'); - Requirements::javascript('unclecheese/dropzone:javascript/file_attachment_field.js'); - if($this->isCMS()) { - Requirements::javascript('unclecheese/dropzone:javascript/file_attachment_field_backend.js'); + Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/dropzone.js'); + Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/file_attachment_field.js'); + if ($this->isCMS()) { + Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/file_attachment_field_backend.js'); } - Requirements::css('unclecheese/dropzone:css/file_attachment_field.css'); + Requirements::css('resources/vendor/unclecheese/dropzone/css/file_attachment_field.css'); - if(!$this->getSetting('url')) { + if (!$this->getSetting('url')) { $this->settings['url'] = $this->Link('upload'); } - if(!$this->getSetting('maxFilesize')) { + if (!$this->getSetting('maxFilesize')) { $this->settings['maxFilesize'] = static::get_filesize_from_ini(); } // The user may not have opted into a multiple upload. If the form field @@ -248,8 +237,8 @@ protected function defineFieldHolderRequirements() $this->settings['uploadMultiple'] = $this->IsMultiple(); // Auto filter images if assigned to an Image relation - if($class = $this->getFileClass()) { - if(Injector::inst()->get($class) instanceof Image) { + if ($class = $this->getFileClass()) { + if (Injector::inst()->get($class) instanceof Image) { $this->imagesOnly(); } } @@ -257,33 +246,31 @@ protected function defineFieldHolderRequirements() /** * Saves the field into a record - * * @param DataObjectInterface $record * @return FileAttachmentField */ public function saveInto(DataObjectInterface $record) { $fieldname = $this->getName(); - if(!$fieldname) { return $this; + if (!$fieldname) { + return $this; } // Handle deletions. This is a bit of a hack. A workaround for having a single form field // post two params. - $deletions = Controller::curr()->getRequest()->postVar('__deletion__'.$this->getName()); + $deletions = Controller::curr()->getRequest()->postVar('__deletion__' . $this->getName()); - if ($deletions && is_array($deletions)) { - foreach($deletions as $id) { + if ($deletions) { + foreach ($deletions as $id) { $this->deleteFileByID($id); } } - $ones = $record->hasOne(); - - if(($relation = $this->getRelation($record))) { - $relation->setByIDList($this->Value()); - } else if(isset($ones[$fieldname])) { + if (($relation = $this->getRelation($record))) { + $relation->setByIDList((array)$this->Value()); + } elseif ($record->has_one($fieldname)) { $record->{"{$fieldname}ID"} = $this->Value() ?: 0; - } elseif($record->hasField($fieldname)) { + } elseif ($record->hasField($fieldname)) { $record->$fieldname = is_array($this->Value()) ? implode(',', $this->Value()) : $this->Value(); } @@ -297,9 +284,8 @@ public function saveInto(DataObjectInterface $record) /** * Set the form method, e.g. PUT - * - * @param string $method - * @return FileAttachmentField + * @param string $method + * @return FileAttachmentField */ public function setMethod($method) { @@ -310,7 +296,6 @@ public function setMethod($method) /** * Return whether files are tracked or not. - * * @return boolean */ public function getTrackFiles() @@ -323,9 +308,8 @@ public function getTrackFiles() /** * Enable/disable file tracking on uploads - * - * @param boolean $bool - * @return FileAttachmentField + * @param boolean $bool + * @return FileAttachmentField */ public function setTrackFiles($bool) { @@ -335,9 +319,8 @@ public function setTrackFiles($bool) /** * Sets number of allowed parallel uploads - * - * @param int $num - * @return FileAttachmentField + * @param int $num + * @return FileAttachmentField */ public function setParallelUploads($num) { @@ -348,9 +331,8 @@ public function setParallelUploads($num) /** * Allow multiple files - * - * @param boolean $bool - * @return FileAttachmentField + * @param boolean $bool + * @return FileAttachmentField */ public function setMultiple($bool) { @@ -362,24 +344,22 @@ public function setMultiple($bool) /** * Max filesize for uploads, in megabytes. * Defaults to upload_max_filesize - * - * @param string $num - * @return FileAttachmentField + * @param string $num + * @return FileAttachmentField */ public function setMaxFilesize($num) { $this->settings['maxFilesize'] = $num; $validator = $this->getValidator(); if ($validator) { - $validator->setAllowedMaxFileSize($num.'m'); + $validator->setAllowedMaxFileSize($num . 'm'); } return $this; } /** * Maximum number of files allowed to be attached - * - * @param int $num + * @param int $num * @return $this */ public function setMaxFiles($num) @@ -392,8 +372,7 @@ public function setMaxFiles($num) /** * Maximum number of files allowed to be attached * (Keeps API consistent with UploadField) - * - * @param int $num + * @param int $num * @return $this */ public function setAllowedMaxFileNumber($num) @@ -403,9 +382,8 @@ public function setAllowedMaxFileNumber($num) /** * Sets the name of the upload parameter, e.g. "Files" - * - * @param string $name - * @return FileAttachmentField + * @param string $name + * @return FileAttachmentField */ public function setParamName($name) { @@ -416,9 +394,8 @@ public function setParamName($name) /** * Allow or disallow image thumbnails created client side - * - * @param boolean $bool - * @return FileAttachmentField + * @param boolean $bool + * @return FileAttachmentField */ public function setCreateImageThumbnails($bool) { @@ -429,9 +406,8 @@ public function setCreateImageThumbnails($bool) /** * Set the threshold at which to not create an image thumbnail - * - * @param int $num - * @return FileAttachmentField + * @param int $num + * @return FileAttachmentField */ public function setMaxThumbnailFilesize($num) { @@ -447,42 +423,34 @@ public function setMaxThumbnailFilesize($num) */ public function addValidFileIDs(array $ids) { - $session = Controller::curr()->getRequest()->getSession(); + $request = Injector::inst()->get(HTTPRequest::class); + $session = $request->getSession(); $validIDs = $session->get('FileAttachmentField.validFileIDs'); - if (!$validIDs) { - $validIDs = array(); + $validIDs = []; } foreach ($ids as $id) { $validIDs[$id] = $id; } - $session->set('FileAttachmentField.validFileIDs', $validIDs); } /** * Get an associative array of File IDs uploaded through this field - * during this session or attached to the file field. + * during this session. * * @return array */ public function getValidFileIDs() { - $session = Controller::curr()->getRequest()->getSession(); - + $request = Injector::inst()->get(HTTPRequest::class); + $session = $request->getSession(); $validIDs = $session->get('FileAttachmentField.validFileIDs'); - - if (!$validIDs || !is_array($validIDs)) { - $validIDs = []; + if ($validIDs && is_array($validIDs)) { + return $validIDs; } - - $all = array_merge( - $validIDs, - $this->AttachedFiles()->column('ID') - ); - - return array_combine($all, $all); + return []; } /** @@ -496,6 +464,21 @@ public function validate($validator) // Detect if files have been removed between AJAX uploads and form submission $value = $this->dataValue(); + if ($value) { + $ids = (array)$value; + $fileCount = (int)File::get()->filter(['ID' => $ids])->count(); + if (count($ids) !== $fileCount) { + $validator->validationError( + $this->name, + _t( + 'FileAttachmentField.MISSINGFILE', + 'Files sent with form have since been removed from the server.' + ), + "validation" + ); + $result = false; + } + } if ($this->hasInvalidFileID) { // If detected invalid file during 'Form::loadDataFrom' @@ -511,25 +494,26 @@ public function validate($validator) "validation" ); $result = false; - } else if ($value && is_array($value)) { - // Prevent a malicious user from inspecting element and changing - // one of the fields to use an invalid File ID. - $validIDs = $this->getValidFileIDs(); - - foreach ($value as $id) { - if (!isset($validIDs[$id])) { - if ($validator) { - $validator->validationError( - $this->name, - _t( - 'FileAttachmentField.VALIDATION', - 'Invalid file ID sent %s.', - array('id' => $id) - ), - "validation" - ); + } else { + if ($value && is_array($value)) { + // Prevent a malicious user from inspecting element and changing + // one of the fields to use an invalid File ID. + $validIDs = $this->getValidFileIDs(); + foreach ($value as $id) { + if (!isset($validIDs[$id])) { + if ($validator) { + $validator->validationError( + $this->name, + _t( + 'FileAttachmentField.VALIDATION', + 'Invalid file ID sent.', + ['id' => $id] + ), + "validation" + ); + } + $result = false; } - $result = false; } } } @@ -538,30 +522,36 @@ public function validate($validator) } /** - * @param int|array $val - * @param array|DataObject $data + * @param int|array $val + * @param array|DataObject $data * @return $this */ - public function setValue($val, $data = array()) + public function setValue($val, $data = []) { if (!$val && $data && $data instanceof DataObject && $data->exists()) { // NOTE: This stops validation errors from occuring when editing // an already saved DataObject. $fieldName = $this->getName(); - $ids = array(); - if ($data->getSchema()->hasOneComponent(get_class($data), $fieldName)) { - $id = $data->{$fieldName.'ID'}; + $ids = []; + if ($data->getSchema()->hasOneComponent($data->getClassName(), $fieldName)) { + $id = $data->{$fieldName . 'ID'}; if ($id) { - $ids[] = $id; + $ids[] = $id; } - } else if ($data->getSchema()->hasManyComponent(get_class($data), $fieldName) || $data->getSchema()->manyManyComponent(get_class($data), $fieldName)) { - $files = $data->{$fieldName}(); - if ($files) { - foreach ($files as $file) { - if (!$file->exists()) { - continue; + } else { + if ( + $data->getSchema()->hasManyComponent($data->getClassName(), $fieldName) + || + $data->getSchema()->manyManyComponent($data->getClassName(), $fieldName) + ) { + $files = $data->{$fieldName}(); + if ($files) { + foreach ($files as $file) { + if (!$file->exists()) { + continue; + } + $ids[] = $file->ID; } - $ids[] = $file->ID; } } } @@ -576,7 +566,6 @@ public function setValue($val, $data = array()) $validIDs = $this->getValidFileIDs(); // NOTE(Jake): If the $data[$name] is an array, its coming from 'loadDataFrom' // If its a single value, its just re-populating the ID on DB data most likely. - if (is_array($data[$this->getName()])) { $ids = &$data[$this->getName()]; foreach ($ids as $i => $id) { @@ -598,9 +587,8 @@ public function setValue($val, $data = array()) /** * The thumbnail width - * - * @param int $num - * @return FileAttachmentField + * @param int $num + * @return FileAttachmentField */ public function setThumbnailWidth($num) { @@ -611,9 +599,8 @@ public function setThumbnailWidth($num) /** * The thumbnail height - * - * @param int $num - * @return FileAttachmentField + * @param int $num + * @return FileAttachmentField */ public function setThumbnailHeight($num) { @@ -624,13 +611,12 @@ public function setThumbnailHeight($num) /** * The layout of the uploader, either "grid" or "list" - * - * @param string $view - * @return FileAttachmentField + * @param string $view + * @return FileAttachmentField */ public function setView($view) { - if(!in_array($view, array ('grid','list'))) { + if (!in_array($view, ['grid', 'list'])) { throw new Exception("FileAttachmentField::setView - View must be one of 'grid' or 'list'"); } @@ -641,7 +627,6 @@ public function setView($view) /** * Gets the current view - * * @return string */ public function getView() @@ -652,9 +637,8 @@ public function getView() /** * Set the selector for the clickable element. Use a boolean for the * entire dropzone. - * - * @param string|bool $val - * @return FileAttachmentField + * @param string|bool $val + * @return FileAttachmentField */ public function setClickable($val) { @@ -665,13 +649,12 @@ public function setClickable($val) /** * A list of accepted file extensions - * - * @param array $files - * @return FileAttachmentField + * @param array $files + * @return FileAttachmentField */ - public function setAcceptedFiles($files = array ()) + public function setAcceptedFiles($files = []) { - if(is_array($files)) { + if (is_array($files)) { $files = implode(',', $files); } $files = str_replace(' ', '', $files); @@ -682,7 +665,7 @@ public function setAcceptedFiles($files = array ()) if ($validator) { $fileExts = explode(',', $files); - $validatorExts = array(); + $validatorExts = []; foreach ($fileExts as $fileExt) { if ($fileExt && isset($fileExt[0]) && $fileExt[0] === '.') { $fileExt = substr($fileExt, 1); @@ -697,25 +680,23 @@ public function setAcceptedFiles($files = array ()) /** * A helper method to only allow images files - * * @return FileAttachmentField */ public function imagesOnly() { - $this->setAcceptedFiles(array('.png','.gif','.jpeg','.jpg')); + $this->setAcceptedFiles(['.png', '.gif', '.jpeg', '.jpg']); return $this; } /** * Sets the allowed mime types - * - * @param array $types - * @return FileAttachmentField + * @param array $types + * @return FileAttachmentField */ - public function setAcceptedMimeTypes($types = array ()) + public function setAcceptedMimeTypes($types = []) { - if(is_array($types)) { + if (is_array($types)) { $types = implode(',', $types); } $this->settings['acceptedMimeTypes'] = $types; @@ -725,9 +706,8 @@ public function setAcceptedMimeTypes($types = array ()) /** * Set auto-processing. If true, uploads happen on addition to the queue - * - * @param boolean $bool - * @return FileAttachmentField + * @param boolean $bool + * @return FileAttachmentField */ public function setAutoProcessQueue($bool) { @@ -739,9 +719,8 @@ public function setAutoProcessQueue($bool) /** * Set the selector for the container element that holds all of the * uploaded files - * - * @param string $val - * @return FileAttachmentField + * @param string $val + * @return FileAttachmentField */ public function setPreviewsContainer($val) { @@ -752,7 +731,6 @@ public function setPreviewsContainer($val) /** * Sets the max resolution for images, in pixels - * * @param int $pixels */ public function setMaxResolution($pixels) @@ -764,7 +742,6 @@ public function setMaxResolution($pixels) /** * Sets the min resolution for images, in pixels - * * @param int $pixels */ public function setMinResolution($pixels) @@ -775,9 +752,8 @@ public function setMinResolution($pixels) /** * Sets selector for the preview template - * - * @param string $template - * @return FileAttachmentField + * @param string $template + * @return FileAttachmentField */ public function setPreviewTemplate($template) { @@ -788,10 +764,9 @@ public function setPreviewTemplate($template) /** * Adds an arbitrary key/val params to send to the server with the upload - * - * @param string $key - * @param mixed $val - * @return FileAttachmentField + * @param string $key + * @param mixed $val + * @return FileAttachmentField */ public function addParam($key, $val) { @@ -803,14 +778,13 @@ public function addParam($key, $val) /** * Sets permissions for this uploader: "detach", "upload", "delete", "attach" * Permissions can be boolean or Callable - * - * @param array $perms - * @return FileAttachmentField + * @param array $perms + * @return FileAttachmentField */ public function setPermissions($perms) { - foreach($perms as $perm => $val) { - if(!isset($this->permissions[$perm])) { + foreach ($perms as $perm => $val) { + if (!isset($this->permissions[$perm])) { throw new Exception("FileAttachmentField::setPermissions - Permission $perm is not allowed"); } $this->permissions[$perm] = $val; @@ -823,17 +797,15 @@ public function setPermissions($perms) * Sets a specific permission for this uploader: "detach", "upload", "delete", "attach" * Permissions can be boolean or Callable * - * @param string $perm - * @param boolean|Callable $val - * @return FileAttachmentField + * @param string $perm + * @param boolean|Callable $val + * @return FileAttachmentField */ public function setPermission($perm, $val) { - return $this->setPermissions( - array( + return $this->setPermissions([ $perm => $val - ) - ); + ]); } /** @@ -855,52 +827,54 @@ public function getDisplayFolderName() /** * Returns true if the uploader is being used in CMS context - * * @return boolean */ public function isCMS() { return Controller::curr() instanceof LeftAndMain; } - + /** - * @note these are user-friendlier versions of internal PHP errors reported back in the ['error'] value of an upload + * @note these are user-friendlier versions of internal PHP errors reported back in the ['error'] value of an upload * @return string */ private function getUploadUserError($code) { $error_message = ""; - switch($code) { - case UPLOAD_ERR_OK: - // no error - 0 - return ""; - break; - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - $error_message = _t('FileAttachmentField.ERRFILESIZE', 'The file is too large, please try again with a smaller version of the file.'); - break; - case UPLOAD_ERR_PARTIAL: - $error_message = _t('FileAttachmentField.ERRPARTIALUPLOAD', 'The file was only partially uploaded, did you cancel the upload? Please try again.'); - break; - case UPLOAD_ERR_NO_FILE: - $error_message = _t('FileAttachmentField.ERRNOFILE', 'No file upload was detected.'); - break; - case UPLOAD_ERR_NO_TMP_DIR: - case UPLOAD_ERR_CANT_WRITE: - case UPLOAD_ERR_EXTENSION: - $error_message = _t('FileAttachmentField.ERRSYSTEMFAIL', 'Sorry, the system is not allowing file uploads at this time.'); - break; - default: - // handles if an extra error value is added at some point as a general error - $error_message = _t('FileAttachmentField.ERRUNKNOWNCODE', 'Sorry, an unknown error has occured. Please try again later.'); - break; + switch ($code) { + case UPLOAD_ERR_OK: + // no error - 0 + return ""; + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $error_message = _t('FileAttachmentField.ERRFILESIZE', + 'The file is too large, please try again with a smaller version of the file.'); + break; + case UPLOAD_ERR_PARTIAL: + $error_message = _t('FileAttachmentField.ERRPARTIALUPLOAD', + 'The file was only partially uploaded, did you cancel the upload? Please try again.'); + break; + case UPLOAD_ERR_NO_FILE: + $error_message = _t('FileAttachmentField.ERRNOFILE', 'No file upload was detected.'); + break; + case UPLOAD_ERR_NO_TMP_DIR: + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + $error_message = _t('FileAttachmentField.ERRSYSTEMFAIL', + 'Sorry, the system is not allowing file uploads at this time.'); + break; + default: + // handles if an extra error value is added at some point as a general error + $error_message = _t('FileAttachmentField.ERRUNKNOWNCODE', + 'Sorry, an unknown error has occured. Please try again later.'); + break; } return $error_message; } /** * Action to handle upload of a single file - * * @note the PHP settings to consider here are file_uploads, upload_max_filesize, post_max_size, upload_tmp_dir * file_uploads - when off, the $_FILES array will be empty * upload_max_filesize - files over this size will trigger error #1 @@ -908,71 +882,74 @@ private function getUploadUserError($code) * upload_tmp_dir - an invalid or non-writable tmp dir will cause error #6 or #7 * @note depending on the size of the uploads allowed, you may like to increase the max input/execution time for these requests * - * @param HTTPRequest $request + * @param HTTPRequest $request * @return HTTPResponse * @return HTTPResponse */ public function upload(HTTPRequest $request) { - + $name = $this->getSetting('paramName'); - $files = (!empty($_FILES[$name]) ? $_FILES[$name] : array()); - $tmpFiles = array(); + $files = (!empty($_FILES[$name]) ? $_FILES[$name] : []); + $tmpFiles = []; // Checking if field is not supporting uploads - if($this->isDisabled() || $this->isReadonly() || !$this->CanUpload()) { - $error_message = _t('FileAttachmentField.UPLOADFORBIDDEN', 'Files cannot be uploaded via this form at the current time.'); + if ($this->isDisabled() || $this->isReadonly() || !$this->CanUpload()) { + $error_message = _t('FileAttachmentField.UPLOADFORBIDDEN', + 'Files cannot be uploaded via this form at the current time.'); return $this->httpError(403, $error_message); } - + // No files detected in the upload, this can occur if post_max_size is < the upload size $value = $request->postVar($name); - if(empty($files) || empty($value)) { - $error_message = _t('FileAttachmentField.NOFILESUPLOADED', 'No files were detected in your upload. Please try again later.'); + if (empty($files) || empty($value)) { + $error_message = _t('FileAttachmentField.NOFILESUPLOADED', + 'No files were detected in your upload. Please try again later.'); return $this->httpError(400, $error_message); } - + // Security token check, must go after above check as a low post_max_size can scrub the Security Token name from the request $form = $this->getForm(); - if($form) { + if ($form) { $token = $form->getSecurityToken(); - if(!$token->checkRequest($request)) { - $error_message = _t('FileAttachmentField.BADSECURITYTOKEN', 'Your form session has expired, please reload the form and try again.'); + if (!$token->checkRequest($request)) { + $error_message = _t('FileAttachmentField.BADSECURITYTOKEN', + 'Your form session has expired, please reload the form and try again.'); return $this->httpError(400, $error_message); } } // Sort the files out into a list of arrays containing each property // http://php.net/manual/en/features.file-upload.post-method.php - if(!empty($files['tmp_name']) && is_array($files['tmp_name'])) { - for($i = 0; $i < count($files['tmp_name']); $i++) { - $tmpFile = array(); - foreach(array('name', 'type', 'tmp_name', 'error', 'size') as $field) { + if (!empty($files['tmp_name']) && is_array($files['tmp_name'])) { + for ($i = 0; $i < count($files['tmp_name']); $i++) { + $tmpFile = []; + foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $field) { $tmpFile[$field] = $files[$field][$i]; } $tmpFiles[] = $tmpFile; } - } - elseif(!empty($files['tmp_name'])) { + } elseif (!empty($files['tmp_name'])) { $tmpFiles[] = $files; } - $ids = array (); - foreach($tmpFiles as $tmpFile) { - if($tmpFile['error']) { + $ids = []; + foreach ($tmpFiles as $tmpFile) { + if ($tmpFile['error']) { // http://php.net/manual/en/features.file-upload.errors.php $user_message = $this->getUploadUserError($tmpFile['error']); return $this->httpError(400, $user_message); } - if($relationClass = $this->getFileClass($tmpFile['name'])) { - $fileObject = Injector::inst()->create($relationClass); + if ($relationClass = $this->getFileClass($tmpFile['name'])) { + $fileObject = new $relationClass(); } try { $this->upload->loadIntoFile($tmpFile, $fileObject, $this->getFolderName()); $ids[] = $fileObject->ID; } catch (Exception $e) { - $error_message = _t('FileAttachmentField.GENERALUPLOADERROR', 'Sorry, the file could not be saved at the current time, please try again later.'); + $error_message = _t('FileAttachmentField.GENERALUPLOADERROR', + 'Sorry, the file could not be saved at the current time, please try again later.'); return $this->httpError(400, $error_message); } @@ -992,11 +969,13 @@ public function upload(HTTPRequest $request) if (!$formController instanceof LeftAndMain) { $trackFile->setRecord($formController->getRecord()); } - } else if ($formClass !== 'Form') { - $trackFile->ControllerClass = $formClass; } else { - // If using generic 'Form' instance, get controller - $trackFile->ControllerClass = $controller->class; + if ($formClass !== Form::class) { + $trackFile->ControllerClass = $formClass; + } else { + // If using generic 'Form' instance, get controller + $trackFile->ControllerClass = $controller->class; + } } $trackFile->FileID = $fileObject->ID; $trackFile->write(); @@ -1004,17 +983,18 @@ public function upload(HTTPRequest $request) } $this->addValidFileIDs($ids); + $this->extend('onAfterUploadFiles', $ids); return new HTTPResponse(implode(',', $ids), 200); } /** - * @param HTTPRequest $request + * @param HTTPRequest $request * @return UploadField_ItemHandler */ public function handleSelect(HTTPRequest $request) { - if($this->isDisabled() || $this->isReadonly() || !$this->CanAttach()) { + if ($this->isDisabled() || $this->isReadonly() || !$this->CanAttach()) { return $this->httpError(403); } @@ -1031,17 +1011,16 @@ public function handleSelect(HTTPRequest $request) */ protected function deleteFileByID($id) { - if($this->CanDelete() && $record = $this->getRecord()) { - $ones = $record->hasOne(); - - if($relation = $this->getRelation()) { + if ($this->CanDelete() && $record = $this->getRecord()) { + if ($relation = $this->getRelation()) { $file = $relation->byID($id); - } - else if(isset($ones[$this->getName()])) { - $file = $record->{$this->getName()}(); + } else { + if ($record->has_one($this->getName())) { + $file = $record->{$this->getName()}(); + } } - if($file && $file->canDelete()) { + if ($file && $file->canDelete()) { $file->delete(); return true; @@ -1054,26 +1033,16 @@ protected function deleteFileByID($id) /** * A template accessor that determines if the uploader is in "multiple" mode * - * @return boolean + * @return boolean */ public function IsMultiple() { - if($this->getSetting('uploadMultiple')) { + if ($this->getSetting('uploadMultiple')) { return true; } - if($record = $this->getRecord()) { - $manyMany = $record->manyMany(); - - if(isset($manyMany[$this->getName()])) { - return true; - } - - $hasMany = $record->hasMany(); - - if(isset($hasMany[$this->getName()])) { - return true; - } + if ($record = $this->getRecord()) { + return ($record->manyMany($this->getName()) || $record->hasMany($this->getName())); } return false; @@ -1082,35 +1051,35 @@ public function IsMultiple() /** * The name of the input, e.g. the "has_one" or "many_many" relation name * - * @return string + * @return string */ public function InputName() { - return $this->IsMultiple() ? $this->getName()."[]" : $this->getName(); + return $this->IsMultiple() ? $this->getName() . "[]" : $this->getName(); } /** * Gets a list of all the files that are attached to the record * - * @return SS_List + * @return SS_List */ public function AttachedFiles() { - if($record = $this->getRecord()) { - if($record->hasMethod($this->getName())) { + if ($record = $this->getRecord()) { + if ($record->hasMethod($this->getName())) { $result = $record->{$this->getName()}(); - - if($result instanceof SS_List) { + if ($result instanceof SS_List) { return $result; - } - else if($result->exists()) { - return ArrayList::create(array($result)); + } else { + if ($result->exists()) { + return ArrayList::create([$result]); + } } } } if ($ids = $this->dataValue()) { - if($ids instanceof ManyManyList) { + if ($ids instanceof ManyManyList) { $ids = array_keys($ids->map()->toArray()); } @@ -1120,7 +1089,7 @@ public function AttachedFiles() $attachments = ArrayList::create(); foreach ($ids as $id) { - $file = File::get()->byID((int) $id); + $file = File::get()->byID((int)$id); if ($file && $file->canView()) { $attachments->push($file); } @@ -1128,37 +1097,38 @@ public function AttachedFiles() return $attachments; } - return new ArrayList(); + return false; } /** * Gets the directory that contains all the file icons organised into sizes * - * @return string + * @return string */ public function RootThumbnailsDir() { - return $this->getSetting('thumbnailsDir') ?: - ModuleResourceLoader::singleton()->resolveURL('unclecheese/dropzone:images/file-icons'); + return $this->getSetting('thumbnailsDir') ?: 'resources/vendor/unclecheese/dropzone/images/file-icons'; } /** * Gets the directory to the file icons for the current thumbnail size * - * @return string + * @return string */ public function ThumbnailsDir() { - return $this->RootThumbnailsDir().'/'.$this->TemplateThumbnailSize()."px"; + return $this->RootThumbnailsDir() . '/' . $this->TemplateThumbnailSize() . "px"; } public function CSSSize() { $w = $this->getSelectedThumbnailWidth(); - if($w < 150) { return "small"; + if ($w < 150) { + return "small"; } - if($w < 250) { return "medium"; + if ($w < 250) { + return "medium"; } return "large"; @@ -1168,18 +1138,17 @@ public function CSSSize() /** * The directory that the module is installed to. A template accessor * - * @return string + * @return string */ public function DropzoneDir() { - return ModuleLoader::inst()->getManifest()->getModule('unclecheese/dropzone') - ->getResourcesDir(); + return basename(dirname(__FILE__)); } /** * Gets the value * - * @return string|array + * @return string|array */ public function Value() { @@ -1189,7 +1158,7 @@ public function Value() /** * Returns true if the "upload" permission returns true * - * @return boolean + * @return boolean */ public function CanUpload() { @@ -1199,7 +1168,7 @@ public function CanUpload() /** * Returns true if the "delete" permission returns true * - * @return boolean + * @return boolean */ public function CanDelete() { @@ -1209,7 +1178,7 @@ public function CanDelete() /** * Returns true if the "detach" permission returns true * - * @return boolean + * @return boolean */ public function CanDetach() { @@ -1219,7 +1188,7 @@ public function CanDetach() /** * Returns true if the "attach" permission returns true * - * @return boolean + * @return boolean */ public function CanAttach() { @@ -1228,7 +1197,6 @@ public function CanAttach() /** * Renders the preview template, optionally for a given file - * * @param int $fileID */ public function PreviewTemplate($fileID = null) @@ -1241,14 +1209,15 @@ public function PreviewTemplate($fileID = null) * Gets the closest thumbnail size for the template, given the list of * icon_sizes (e.g. 32px, 64px, 128px) * - * @return int + * @return int */ public function TemplateThumbnailSize() { $w = $this->getSelectedThumbnailWidth(); - foreach($this->config()->icon_sizes as $size) { - if($w <= $size) { return $size; + foreach ($this->config()->icon_sizes as $size) { + if ($w <= $size) { + return $size; } } } @@ -1256,27 +1225,27 @@ public function TemplateThumbnailSize() /** * Returns true if the uploader auto-processes * - * @return boolean + * @return boolean */ public function AutoProcess() { - $result = (bool) $this->getSetting('autoProcessQueue'); + $result = (bool)$this->getSetting('autoProcessQueue'); return $result; } /** * Checks for a given permission. If it is a closure, invoke the method - * * @param string $perm * @return boolean */ protected function checkPerm($perm) { - if(!isset($this->permissions[$perm])) { return false; + if (!isset($this->permissions[$perm])) { + return false; } - if(is_callable($this->permissions[$perm])) { + if (is_callable($this->permissions[$perm])) { return $this->permissions[$perm](); } @@ -1298,23 +1267,24 @@ public function getFileClass($filename = null) $name = $this->getName(); $record = $this->getRecord(); - $ext = pathinfo($filename, PATHINFO_EXTENSION); + $ext = pathinfo($filename ?? '', PATHINFO_EXTENSION); $defaultClass = File::get_class_for_file_extension($ext); - if(empty($name) || empty($record)) { + if (empty($name) || empty($record)) { return $defaultClass; } - if($record) { + if ($record) { $class = $record->getRelationClass($name); - if(!$class) { $class = File::class; + if (!$class) { + $class = File::class; } } - if($filename) { - if($defaultClass == "Image" - && $this->config()->upgrade_images - && !Injector::inst()->get($class) instanceof Image + if ($filename) { + if ($defaultClass == Image::class && + $this->config()->upgrade_images && + !Injector::inst()->get($class) instanceof Image ) { $class = Image::class; } @@ -1325,31 +1295,19 @@ public function getFileClass($filename = null) /** * Get the record that this form field is editing - * * @return DataObject */ public function getRecord() { if (!$this->record && $this->form) { - $record = $this->form->getRecord(); - if ($record && $record instanceof DataObject) { + 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; - } - else if ($controller = $this->form->getController()) { - if($controller->hasMethod('data') - && ($record = $controller->data()) - && ($record instanceof DataObject) - ) { - $this->record = $record; - } else if($controller->hasMethod('getRecord')) { - if($controller->hasMethod('currentPageID')) { - if($record = $controller->getRecord($controller->currentPageID())) { - $this->record = $record; - } - } else { - $this->record = $controller->getRecord(); - } - } } } @@ -1358,15 +1316,15 @@ public function getRecord() /** * Gets the name of the relation, if attached to a record - * * @return string */ protected function getRelation($record = null) { - if(!$record) { $record = $this->getRecord(); + if (!$record) { + $record = $this->getRecord(); } - if($record) { + if ($record) { $fieldname = $this->getName(); $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; @@ -1386,7 +1344,7 @@ protected function getRelation($record = null) */ protected function getSetting($setting) { - if(isset($this->settings[$setting])) { + if (isset($this->settings[$setting])) { return $this->settings[$setting]; } @@ -1404,11 +1362,9 @@ protected function getSetting($setting) */ protected function getDefaults() { - $file_path = ModuleLoader::inst()->getManifest()->getModule('unclecheese/dropzone') - ->getResource($this->config()->default_config_path) - ->getPath(); - if(!file_exists($file_path)) { - throw new Exception("FileAttachmentField::getDefaults() - There is no config json file at $file_path"); + $file_path = BASE_PATH . '/' . 'vendor/unclecheese/dropzone/' . $this->config()->default_config_path; + if (!file_exists($file_path)) { + throw new \Exception("FileAttachmentField::getDefaults() - There is no config json file at $file_path"); } return Convert::json2array(file_get_contents($file_path)); @@ -1416,12 +1372,11 @@ protected function getDefaults() /** * Gets the thumbnail width given the current view type - * * @return int */ public function getSelectedThumbnailWidth() { - if($w = $this->getSetting('thumbnailWidth')) { + if ($w = $this->getSetting('thumbnailWidth')) { return $w; } @@ -1432,12 +1387,11 @@ public function getSelectedThumbnailWidth() /** * Gets the thumbnail height given the current view type - * * @return int */ public function getSelectedThumbnailHeight() { - if($h = $this->getSetting('thumbnailHeight')) { + if ($h = $this->getSetting('thumbnailHeight')) { return $h; } @@ -1456,16 +1410,16 @@ public function getConfigJSON() { $data = $this->settings; $defaults = $this->getDefaults(); - foreach($this->config()->defaults as $setting => $value) { + foreach ($this->config()->defaults as $setting => $value) { $js_name = static::camelise($setting); // If the setting has been set on the instance, use that value - if(isset($data[$js_name])) { + if (isset($data[$js_name])) { continue; } // Only include the setting in the JSON if it differs from the core default value - if(!isset($defaults[$js_name]) || ($defaults[$js_name] !== $value)) { + if (!isset($defaults[$js_name]) || ($defaults[$js_name] !== $value)) { $data[$js_name] = $value; } } @@ -1475,35 +1429,17 @@ public function getConfigJSON() $data['thumbnailWidth'] = $this->getSelectedThumbnailWidth(); $data['thumbnailHeight'] = $this->getSelectedThumbnailHeight(); - if(!$this->IsMultiple()) { + if (!$this->IsMultiple()) { $data['maxFiles'] = 1; } - if($this->isCMS()) { + if ($this->isCMS()) { $data['urlSelectDialog'] = $this->Link('select'); - if($this->getFolderName()) { + if ($this->getFolderName()) { $data['folderID'] = Folder::find_or_make($this->getFolderName())->ID; } } return Convert::array2json($data); } - - public function performReadonlyTransformation() - { - $readonly = clone $this; - $readonly->setPermissions( - [ - 'attach' => false, - 'detach' => false, - 'upload' => false, - 'delete' => false - ] - ); - - $readonly->setReadonly(true); - $readonly->addExtraClass('readonly'); - - return $readonly; - } } diff --git a/src/FileAttachmentField_SelectHandler.php b/src/FileAttachmentField_SelectHandler.php new file mode 100644 index 0000000..8c2facf --- /dev/null +++ b/src/FileAttachmentField_SelectHandler.php @@ -0,0 +1,100 @@ +setValue($folderID); + + // Generate the file list field. + $config = GridFieldConfig::create(); + $config->addComponent(new GridFieldSortableHeader()); + $config->addComponent(new GridFieldFilterHeader()); + $config->addComponent($columns = new GridFieldDataColumns()); + $columns->setDisplayFields(array( + 'StripThumbnail' => '', + 'Name' => 'Name', + 'Title' => 'Title' + )); + $config->addComponent(new GridFieldPaginator(8)); + + // If relation is to be autoset, we need to make sure we only list compatible objects. + $baseClass = $this->parent->getFileClass(); + + // Create the data source for the list of files within the current directory. + $files = DataList::create($baseClass)->filter('ParentID', $folderID); + + $fileField = new GridField('Files', false, $files, $config); + $fileField->setAttribute('data-selectable', true); + if($this->parent->IsMultiple()) { + $fileField->setAttribute('data-multiselect', true); + } + + $selectComposite = new CompositeField( + $folderField, + $fileField + ); + + return $selectComposite; + } + + + public function filesbyid(HTTPRequest $r) { + $ids = $r->getVar('ids'); + $files = File::get()->byIDs(explode(',',$ids)); + + $validIDs = array(); + $json = array (); + foreach($files as $file) { + $template = new SSViewer('FileAttachmentField_attachments'); + $html = $template->process(ArrayData::create(array( + 'File' => $file, + 'Scope' => $this->parent + ))); + + $validIDs[$file->ID] = $file->ID; + $json[] = array ( + 'id' => $file->ID, + 'html' => $html->forTemplate() + ); + } + + $this->parent->addValidFileIDs($validIDs); + return Convert::array2json($json); + } + +} diff --git a/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss b/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss index 8e9ef31..8dd857d 100644 --- a/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss +++ b/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss @@ -1,32 +1,30 @@ -
  • - - - - - - - - - - - - <%t Dropzone.ADDEDNOW 'Added just now' %> - · $File.Size - - - - - - - - - - - - -
    <%t Dropzone.ERROR 'Oh no!' %>
    - - -
    -
    -
  • +
  • + + + + + + + + + + + <%t Dropzone.ADDEDNOW 'Added just now' %> + · $File.Size + + + + + + + + + + + +
    <%t Dropzone.ERROR 'Oh no!' %>
    + + +
    +
    +
  • diff --git a/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss b/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss index 79616eb..d40dcba 100644 --- a/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss +++ b/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss @@ -1,50 +1,49 @@ -
  • -1 %>dropzone-image<% else %>dropzone-file<% end_if %>" style="height:{$Scope.SelectedThumbnailHeight}px;<% if $Scope.View == 'grid' %>width:{$Scope.SelectedThumbnailWidth}px;<% end_if %>" > - $Scope.SelectedThumbnailHeight %> - style="height:{$Scope.SelectedThumbnailHeight}px" - <% else %> - style="width:{$Scope.SelectedThumbnailWidth}px" - <% end_if %> - <% if $File.IsImage && $File.Orientation > -1 %> - src="$File.Fill($Scope.SelectedThumbnailWidth, $Scope.SelectedThumbnailHeight).URL" - <% else %> - src="$Scope.ThumbnailsDir/{$File.Extension.LowerCase}.png" onerror="this.src='$Scope.ThumbnailsDir/_blank.png'" onload="this.parentNode.style.backgroundImage='url('+this.src+')';this.style.display='none';" - <% end_if %> - > + $Scope.SelectedThumbnailHeight %> + style="height:{$Scope.SelectedThumbnailHeight}px" + <% else %> + style="width:{$Scope.SelectedThumbnailWidth}px" + <% end_if %> + <% if $File.Orientation > -1 %> + src="$File.CroppedImage($Scope.SelectedThumbnailWidth, $Scope.SelectedThumbnailHeight).URL" + <% else %> + src="$Scope.ThumbnailsDir/{$File.Extension.LowerCase}.png" onerror="this.src='$Scope.ThumbnailsDir/_blank.png'" onload="this.parentNode.style.backgroundImage='url('+this.src+')';this.style.display='none';" + <% end_if %> + > $File.Title - <%t Dropzone.ADDEDON 'Added on {date}' date=$File.Created.Format('j M Y') %> - · $File.Size + <%t Dropzone.ADDEDON 'Added on {date}' date=$File.Created.Format('j M Y') %> + · $File.Size <% if $Scope.CanDetach %> <%t Dropzone.DETACHFILE 'remove' %> - + <% end_if %> <% if $Scope.CanDelete %> <%t Dropzone.MARKFORDELETION 'delete' %> - + <% end_if %> - <% if $Scope.CanDetach %>
    <%t Dropzone.REMOVED 'removed' %>
    <%t Dropzone.CHANGEAFTERSAVE 'The change will take effect after you save.' %> - +
    <% end_if %> @@ -53,7 +52,7 @@
    <%t Dropzone.DELETED 'deleted' %>
    <%t Dropzone.CHANGEAFTERSAVE 'The change will take effect after you save.' %> - +
    <% end_if %>