From 5712e21a8657574a10efc2342d62ed9c1a4e1490 Mon Sep 17 00:00:00 2001 From: Pack Date: Tue, 10 Sep 2024 17:36:36 +1200 Subject: [PATCH 1/7] import old code work from plastic studio --- composer.json | 36 +- src/DropzoneFile.php | 151 +- src/FileAttachmentField.php | 2910 ++++++++--------- src/FileAttachmentField_SelectHandler.php | 93 + .../Dropzone/FileAttachmentField_preview.ss | 62 +- .../FileAttachmentField_attachments.ss | 57 +- 6 files changed, 1667 insertions(+), 1642 deletions(-) create mode 100644 src/FileAttachmentField_SelectHandler.php 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..f38cd8c 100644 --- a/src/DropzoneFile.php +++ b/src/DropzoneFile.php @@ -1,92 +1,89 @@ */ class DropzoneFile extends DataExtension { - - - /** - * Helper method for determining if this is an Image - * - * @return boolean - */ - public function IsImage() - { - return $this->owner instanceof Image; - } - - - /** - * 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(!$h) { $h = $this->owner->config()->grid_thumbnail_height; - } - - if($this->IsImage() && Director::fileExists($this->owner->Filename)) { - return $this->owner->CroppedImage($w, $h); - } - - $sizes = Config::inst()->forClass(FileAttachmentField::class)->icon_sizes; - sort($sizes); - - foreach($sizes as $size) { - if($w <= $size) { - if($this->owner instanceof Folder) { - $file = $this->getFilenameForType('_folder', $size); - } - else { - $file = $this->getFilenameForType($this->owner->getExtension(), $size); - } - if(!file_exists(BASE_PATH.'/'.$file)) { - $file = $this->getFilenameForType('_blank', $size); - } - - $image = Image::create(); - $image->setFromLocalFile(Director::getAbsFile($file), basename($file)); - - return $image; - } - } - } - - - /** - * 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 - * @return string - */ - protected function getFilenameForType($ext, $size) - { - return ModuleResourceLoader::singleton()->resolveURL(sprintf( - 'unclecheese/dropzone:images/file-icons/%spx/%s.png', - $size, - strtolower($ext) - )); - } -} \ No newline at end of file + + + /** + * Helper method for determining if this is an Image + * + * @return boolean + */ + public function IsImage() + { + return $this->owner instanceof Image; + } + + + /** + * 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 (!$h) { + $h = $this->owner->config()->grid_thumbnail_height; + } + + if ($this->IsImage() && Director::fileExists($this->owner->Filename)) { + return $this->owner->CroppedImage($w, $h); + } + + $sizes = Config::inst()->forClass('FileAttachmentField')->icon_sizes; + sort($sizes); + + foreach ($sizes as $size) { + if ($w <= $size) { + if ($this->owner instanceof Folder) { + $file = $this->getFilenameForType('_folder', $size); + } else { + $file = $this->getFilenameForType($this->owner->getExtension(), $size); + } + if (!file_exists(BASE_PATH . '/' . $file)) { + $file = $this->getFilenameForType('_blank', $size); + } + + return new File(Director::makeRelative($file)); + } + } + } + + + /** + * 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 + * @return string + */ + protected function getFilenameForType($ext, $size) + { + return sprintf( + '%s/images/file-icons/%spx/%s.png', + basename(dirname(__FILE__)), + $size, + strtolower($ext) + ); + } +} diff --git a/src/FileAttachmentField.php b/src/FileAttachmentField.php index ddea293..e85ab4c 100644 --- a/src/FileAttachmentField.php +++ b/src/FileAttachmentField.php @@ -1,1509 +1,1441 @@ */ class FileAttachmentField extends FileField { - - /** - * The allowed actions for the RequestHandler - * - * @var array - */ - private static $allowed_actions = array ( - 'upload', - 'handleSelect', - ); - - - private static $url_handlers = array ( - 'select' => 'handleSelect', - ); - - /** - * Track files that are uploaded and remove the tracked files when - * they are saved into a record. - * - * @var boolean - */ - private static $track_files = false; - - /** - * 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; - - /** - * A list of custom permissions for this instance - * Options available: - * - upload - * - 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'; - - /** - * 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'; - - /** - * 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 - ); - } - - /** - * Translate camelCase to underscore_case - * - * @param string $str - * @return string - */ - public static function underscorise($str) - { - $str[0] = strtolower($str[0]); - - return preg_replace_callback( - '/([A-Z])/', function ($c) { - return "_" . strtolower($c[1]); - }, $str - ); - } - - /** - * Looks at the php.ini and takes the lower of two values, translates it into - * an int representing the number of bytes allowed per upload - * - * @return int - */ - public static function get_filesize_from_ini() - { - $bytes = min( - array( - File::ini2bytes(ini_get('post_max_size') ?: '8M'), - File::ini2bytes(ini_get('upload_max_filesize') ?: '2M') - ) - ); - - return floor($bytes/(1024*1024)); - } - - /** - * Constructor. Sets some default permissions - * - * @param string $name - * @param string $title - * @param string $value - * @param Form $form - */ - public function __construct($name, $title = null, $value = null, $form = null) - { - $instance = $this; - - $this->permissions['upload'] = true; - $this->permissions['detach'] = true; - $this->permissions['delete'] = function () use ($instance) { - return Injector::inst()->get(File::class)->canDelete() && $instance->isCMS(); - }; - $this->permissions['attach'] = function () use ($instance) { - return $instance->isCMS(); - }; - - $this->setFieldHolderTemplate(__NAMESPACE__ . '\\FileAttachmentField_holder'); - $this->setSmallFieldHolderTemplate(__NAMESPACE__ . '\\FileAttachmentField_holder_small'); - - parent::__construct($name, $title, $value, $form); - } - - /** - * Renders the form field, loads requirements. Sets file size based on php.ini - * Adds the security token - * - * @param array $attributes - * @return SSViewer - */ - public function FieldHolder($attributes = array ()) - { - $this->defineFieldHolderRequirements(); - return parent::FieldHolder($attributes); - } - - /** - * Renders the small form field holder, loads requirements. Sets file size based on php.ini - * Adds the security token - * - * @param array $attributes - * @return SSViewer - */ - public function SmallFieldHolder($attributes = array ()) - { - $this->defineFieldHolderRequirements(); - return parent::SmallFieldHolder($attributes); - } - - /** - * Define some requirements and settings just before rendering the Field Holder. - */ - 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::css('unclecheese/dropzone:css/file_attachment_field.css'); - - if(!$this->getSetting('url')) { - $this->settings['url'] = $this->Link('upload'); - } - - 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 - // is attached to a record that has a multi relation, set that automatically. - $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) { - $this->imagesOnly(); - } - } - } - - /** - * Saves the field into a record - * - * @param DataObjectInterface $record - * @return FileAttachmentField - */ - public function saveInto(DataObjectInterface $record) - { - $fieldname = $this->getName(); - 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()); - - if ($deletions && is_array($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])) { - $record->{"{$fieldname}ID"} = $this->Value() ?: 0; - } elseif($record->hasField($fieldname)) { - $record->$fieldname = is_array($this->Value()) ? implode(',', $this->Value()) : $this->Value(); - } - - if ($this->getTrackFiles()) { - $fileIDs = (array)$this->Value(); - FileAttachmentFieldTrack::untrack($fileIDs); - } - - return $this; - } - - /** - * Set the form method, e.g. PUT - * - * @param string $method - * @return FileAttachmentField - */ - public function setMethod($method) - { - $this->settings['method'] = $method; - - return $this; - } - - /** - * Return whether files are tracked or not. - * - * @return boolean - */ - public function getTrackFiles() - { - if (isset($this->settings['trackFiles']) && $this->settings['trackFiles'] !== null) { - return $this->settings['trackFiles']; - } - return $this->config()->track_files; - } - - /** - * Enable/disable file tracking on uploads - * - * @param boolean $bool - * @return FileAttachmentField - */ - public function setTrackFiles($bool) - { - $this->settings['trackFiles'] = $bool; - return $this; - } - - /** - * Sets number of allowed parallel uploads - * - * @param int $num - * @return FileAttachmentField - */ - public function setParallelUploads($num) - { - $this->settings['parallelUploads'] = $num; - - return $this; - } - - /** - * Allow multiple files - * - * @param boolean $bool - * @return FileAttachmentField - */ - public function setMultiple($bool) - { - $this->settings['uploadMultiple'] = $bool; - - return $this; - } - - /** - * Max filesize for uploads, in megabytes. - * Defaults to upload_max_filesize - * - * @param string $num - * @return FileAttachmentField - */ - public function setMaxFilesize($num) - { - $this->settings['maxFilesize'] = $num; - $validator = $this->getValidator(); - if ($validator) { - $validator->setAllowedMaxFileSize($num.'m'); - } - return $this; - } - - /** - * Maximum number of files allowed to be attached - * - * @param int $num - * @return $this - */ - public function setMaxFiles($num) - { - $this->settings['maxFiles'] = $num; - - return $this; - } - - /** - * Maximum number of files allowed to be attached - * (Keeps API consistent with UploadField) - * - * @param int $num - * @return $this - */ - public function setAllowedMaxFileNumber($num) - { - return $this->setMaxFiles($num); - } - - /** - * Sets the name of the upload parameter, e.g. "Files" - * - * @param string $name - * @return FileAttachmentField - */ - public function setParamName($name) - { - $this->settings['paramName'] = $name; - - return $this; - } - - /** - * Allow or disallow image thumbnails created client side - * - * @param boolean $bool - * @return FileAttachmentField - */ - public function setCreateImageThumbnails($bool) - { - $this->settings['createImageThumbnails'] = $bool; - - return $this; - } - - /** - * Set the threshold at which to not create an image thumbnail - * - * @param int $num - * @return FileAttachmentField - */ - public function setMaxThumbnailFilesize($num) - { - $this->settings['thumbnailFilesize'] = $num; - - return $this; - } - - /** - * Add an array of IDs - * - * @return void - */ - public function addValidFileIDs(array $ids) - { - $session = Controller::curr()->getRequest()->getSession(); - - $validIDs = $session->get('FileAttachmentField.validFileIDs'); - - if (!$validIDs) { - $validIDs = array(); - } - 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. - * - * @return array - */ - public function getValidFileIDs() - { - $session = Controller::curr()->getRequest()->getSession(); - - $validIDs = $session->get('FileAttachmentField.validFileIDs'); - - if (!$validIDs || !is_array($validIDs)) { - $validIDs = []; - } - - $all = array_merge( - $validIDs, - $this->AttachedFiles()->column('ID') - ); - - return array_combine($all, $all); - } - - /** - * Check that the user is submitting the file IDs that they uploaded. - * - * @return boolean - */ - public function validate($validator) - { - $result = true; - - // Detect if files have been removed between AJAX uploads and form submission - $value = $this->dataValue(); - - if ($this->hasInvalidFileID) { - // If detected invalid file during 'Form::loadDataFrom' - // (Below validation isn't triggered as setValue() removes the invalid ID - // to prevent the CMS from loading something it shouldn't, also stops the - // validator from realizing there's an invalid ID.) - $validator->validationError( - $this->name, - _t( - 'FileAttachmentField.VALIDATION', - 'Invalid file ID sent.' - ), - "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" - ); - } - $result = false; - } - } - } - - return $result; - } - - /** - * @param int|array $val - * @param array|DataObject $data - * @return $this - */ - public function setValue($val, $data = array()) - { - 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'}; - if ($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; - } - $ids[] = $file->ID; - } - } - } - if ($ids) { - $this->addValidFileIDs($ids); - } - } - if ($data && is_array($data) && isset($data[$this->getName()])) { - // Prevent Form::loadDataFrom() from loading invalid File IDs - // that may have been passed. - $isInvalid = false; - $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) { - if ($validIDs && !isset($validIDs[$id])) { - unset($ids[$i]); - $isInvalid = true; - } - } - if ($isInvalid) { - $ids = array_values($ids); - $val = $ids; - $this->hasInvalidFileID = true; - } - unset($ids); // stop $ids variable from modifying to $data array. - } - } - return parent::setValue($val, $data); - } - - /** - * The thumbnail width - * - * @param int $num - * @return FileAttachmentField - */ - public function setThumbnailWidth($num) - { - $this->settings['thumbnailWidth'] = $num; - - return $this; - } - - /** - * The thumbnail height - * - * @param int $num - * @return FileAttachmentField - */ - public function setThumbnailHeight($num) - { - $this->settings['thumbnailHeight'] = $num; - - return $this; - } - - /** - * The layout of the uploader, either "grid" or "list" - * - * @param string $view - * @return FileAttachmentField - */ - public function setView($view) - { - if(!in_array($view, array ('grid','list'))) { - throw new Exception("FileAttachmentField::setView - View must be one of 'grid' or 'list'"); - } - - $this->view = $view; - - return $this; - } - - /** - * Gets the current view - * - * @return string - */ - public function getView() - { - return $this->view; - } - - /** - * Set the selector for the clickable element. Use a boolean for the - * entire dropzone. - * - * @param string|bool $val - * @return FileAttachmentField - */ - public function setClickable($val) - { - $this->settings['clickable'] = $val; - - return $this; - } - - /** - * A list of accepted file extensions - * - * @param array $files - * @return FileAttachmentField - */ - public function setAcceptedFiles($files = array ()) - { - if(is_array($files)) { - $files = implode(',', $files); - } - $files = str_replace(' ', '', $files); - $this->settings['acceptedFiles'] = $files; - - // Update validator - $validator = $this->getValidator(); - if ($validator) { - $fileExts = explode(',', $files); - - $validatorExts = array(); - foreach ($fileExts as $fileExt) { - if ($fileExt && isset($fileExt[0]) && $fileExt[0] === '.') { - $fileExt = substr($fileExt, 1); - } - $validatorExts[] = $fileExt; - } - $validator->setAllowedExtensions($validatorExts); - } - - return $this; - } - - /** - * A helper method to only allow images files - * - * @return FileAttachmentField - */ - public function imagesOnly() - { - $this->setAcceptedFiles(array('.png','.gif','.jpeg','.jpg')); - - return $this; - } - - /** - * Sets the allowed mime types - * - * @param array $types - * @return FileAttachmentField - */ - public function setAcceptedMimeTypes($types = array ()) - { - if(is_array($types)) { - $types = implode(',', $types); - } - $this->settings['acceptedMimeTypes'] = $types; - - return $this; - } - - /** - * Set auto-processing. If true, uploads happen on addition to the queue - * - * @param boolean $bool - * @return FileAttachmentField - */ - public function setAutoProcessQueue($bool) - { - $this->settings['autoProcessQueue'] = $bool; - - return $this; - } - - /** - * Set the selector for the container element that holds all of the - * uploaded files - * - * @param string $val - * @return FileAttachmentField - */ - public function setPreviewsContainer($val) - { - $this->settings['previewsContainer'] = $val; - - return $this; - } - - /** - * Sets the max resolution for images, in pixels - * - * @param int $pixels - */ - public function setMaxResolution($pixels) - { - $this->settings['maxResolution'] = $pixels; - - return $this; - } - - /** - * Sets the min resolution for images, in pixels - * - * @param int $pixels - */ - public function setMinResolution($pixels) - { - $this->settings['minResolution'] = $pixels; - return $this; - } - - /** - * Sets selector for the preview template - * - * @param string $template - * @return FileAttachmentField - */ - public function setPreviewTemplate($template) - { - $this->previewTemplate = $template; - - return $this; - } - - /** - * Adds an arbitrary key/val params to send to the server with the upload - * - * @param string $key - * @param mixed $val - * @return FileAttachmentField - */ - public function addParam($key, $val) - { - $this->params[$key] = $val; - - return $this; - } - - /** - * Sets permissions for this uploader: "detach", "upload", "delete", "attach" - * Permissions can be boolean or Callable - * - * @param array $perms - * @return FileAttachmentField - */ - public function setPermissions($perms) - { - foreach($perms as $perm => $val) { - if(!isset($this->permissions[$perm])) { - throw new Exception("FileAttachmentField::setPermissions - Permission $perm is not allowed"); - } - $this->permissions[$perm] = $val; - } - - return $this; - } - - /** - * 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 - */ - public function setPermission($perm, $val) - { - return $this->setPermissions( - array( - $perm => $val - ) - ); - } - - /** - * @param String - */ - public function setDisplayFolderName($name) - { - $this->displayFolderName = $name; - return $this; - } - - /** - * @return String - */ - public function getDisplayFolderName() - { - return $this->displayFolderName; - } - - /** - * 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 - * @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; - } - 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 - * post_max_size - requests over this size will cause the $_FILES array to be empty - * 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 - * @return HTTPResponse - * @return HTTPResponse - */ - public function upload(HTTPRequest $request) - { - - $name = $this->getSetting('paramName'); - $files = (!empty($_FILES[$name]) ? $_FILES[$name] : array()); - $tmpFiles = array(); - - // 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.'); - 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.'); - 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) { - $token = $form->getSecurityToken(); - 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) { - $tmpFile[$field] = $files[$field][$i]; - } - $tmpFiles[] = $tmpFile; - } - } - elseif(!empty($files['tmp_name'])) { - $tmpFiles[] = $files; - } - - $ids = array (); - 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); - } - - 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.'); - return $this->httpError(400, $error_message); - } - - if ($this->upload->isError()) { - return $this->httpError(400, implode(' ' . PHP_EOL, $this->upload->getErrors())); - } - - if ($this->getTrackFiles()) { - $controller = Controller::has_curr() ? Controller::curr() : null; - $formClass = ($form) ? get_class($form) : ''; - - $trackFile = FileAttachmentFieldTrack::create(); - if ($controller instanceof LeftAndMain) { - // If in CMS (store DataObject or Page) - $formController = $form->getController(); - $trackFile->ControllerClass = $formController->class; - 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; - } - $trackFile->FileID = $fileObject->ID; - $trackFile->write(); - } - } - - $this->addValidFileIDs($ids); - return new HTTPResponse(implode(',', $ids), 200); - } - - - /** - * @param HTTPRequest $request - * @return UploadField_ItemHandler - */ - public function handleSelect(HTTPRequest $request) - { - if($this->isDisabled() || $this->isReadonly() || !$this->CanAttach()) { - return $this->httpError(403); - } - - return FileAttachmentField_SelectHandler::create($this, $this->getFolderName()); - } - - - /** - * Deletes a file. Ensures user has permissions and the file is part - * of the current record, so as not to allow arbitrary deletion of files - * - * @param int $id - * @return boolean - */ - protected function deleteFileByID($id) - { - if($this->CanDelete() && $record = $this->getRecord()) { - $ones = $record->hasOne(); - - if($relation = $this->getRelation()) { - $file = $relation->byID($id); - } - else if(isset($ones[$this->getName()])) { - $file = $record->{$this->getName()}(); - } - - if($file && $file->canDelete()) { - $file->delete(); - - return true; - } - } - - return false; - } - - /** - * A template accessor that determines if the uploader is in "multiple" mode - * - * @return boolean - */ - public function IsMultiple() - { - 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; - } - } - - return false; - } - - /** - * The name of the input, e.g. the "has_one" or "many_many" relation name - * - * @return string - */ - public function InputName() - { - return $this->IsMultiple() ? $this->getName()."[]" : $this->getName(); - } - - /** - * Gets a list of all the files that are attached to the record - * - * @return SS_List - */ - public function AttachedFiles() - { - if($record = $this->getRecord()) { - if($record->hasMethod($this->getName())) { - $result = $record->{$this->getName()}(); - - if($result instanceof SS_List) { - return $result; - } - else if($result->exists()) { - return ArrayList::create(array($result)); - } - } - } - - if ($ids = $this->dataValue()) { - if($ids instanceof ManyManyList) { - $ids = array_keys($ids->map()->toArray()); - } - - if (!is_array($ids)) { - $ids = explode(',', $ids); - } - - $attachments = ArrayList::create(); - foreach ($ids as $id) { - $file = File::get()->byID((int) $id); - if ($file && $file->canView()) { - $attachments->push($file); - } - } - return $attachments; - } - - return new ArrayList(); - } - - /** - * Gets the directory that contains all the file icons organised into sizes - * - * @return string - */ - public function RootThumbnailsDir() - { - return $this->getSetting('thumbnailsDir') ?: - ModuleResourceLoader::singleton()->resolveURL('unclecheese/dropzone:images/file-icons'); - } - - /** - * Gets the directory to the file icons for the current thumbnail size - * - * @return string - */ - public function ThumbnailsDir() - { - return $this->RootThumbnailsDir().'/'.$this->TemplateThumbnailSize()."px"; - } - - - public function CSSSize() - { - $w = $this->getSelectedThumbnailWidth(); - if($w < 150) { return "small"; - } - if($w < 250) { return "medium"; - } - - return "large"; - } - - - /** - * The directory that the module is installed to. A template accessor - * - * @return string - */ - public function DropzoneDir() - { - return ModuleLoader::inst()->getManifest()->getModule('unclecheese/dropzone') - ->getResourcesDir(); - } - - /** - * Gets the value - * - * @return string|array - */ - public function Value() - { - return $this->dataValue(); - } - - /** - * Returns true if the "upload" permission returns true - * - * @return boolean - */ - public function CanUpload() - { - return $this->checkPerm('upload'); - } - - /** - * Returns true if the "delete" permission returns true - * - * @return boolean - */ - public function CanDelete() - { - return $this->checkPerm('delete'); - } - - /** - * Returns true if the "detach" permission returns true - * - * @return boolean - */ - public function CanDetach() - { - return $this->checkPerm('detach'); - } - - /** - * Returns true if the "attach" permission returns true - * - * @return boolean - */ - public function CanAttach() - { - return $this->checkPerm('attach'); - } - - /** - * Renders the preview template, optionally for a given file - * - * @param int $fileID - */ - public function PreviewTemplate($fileID = null) - { - return $this->renderWith($this->previewTemplate); - - } - - /** - * Gets the closest thumbnail size for the template, given the list of - * icon_sizes (e.g. 32px, 64px, 128px) - * - * @return int - */ - public function TemplateThumbnailSize() - { - $w = $this->getSelectedThumbnailWidth(); - - foreach($this->config()->icon_sizes as $size) { - if($w <= $size) { return $size; - } - } - } - - /** - * Returns true if the uploader auto-processes - * - * @return boolean - */ - public function AutoProcess() - { - $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(is_callable($this->permissions[$perm])) { - return $this->permissions[$perm](); - } - - return $this->permissions[$perm]; - } - - /** - * Gets the classname for the file, e.g. from the declared - * file relation on the record. - * - * If given a filename, look at the extension and upgrade it - * to an Image if necessary. - * - * @param string $filename - * @return string - */ - public function getFileClass($filename = null) - { - $name = $this->getName(); - $record = $this->getRecord(); - - $ext = pathinfo($filename, PATHINFO_EXTENSION); - $defaultClass = File::get_class_for_file_extension($ext); - - if(empty($name) || empty($record)) { - return $defaultClass; - } - - if($record) { - $class = $record->getRelationClass($name); - if(!$class) { $class = File::class; - } - } - - if($filename) { - if($defaultClass == "Image" - && $this->config()->upgrade_images - && !Injector::inst()->get($class) instanceof Image - ) { - $class = Image::class; - } - } - - return $class; - } - - /** - * 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) { - $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(); - } - } - } - } - - return $this->record; - } - - /** - * 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) { - $fieldname = $this->getName(); - $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; - - return ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) ? $relation : false; - } - - return false; - } - - /** - * Gets a given setting. Falls back on Config defaults - * - * Note: config settings are in underscore_case - * - * @param string $setting - * @return mixed - */ - protected function getSetting($setting) - { - if(isset($this->settings[$setting])) { - return $this->settings[$setting]; - } - - $config = Config::inst()->get(__CLASS__, "defaults"); - $configName = static::underscorise($setting); - - return isset($config[$configName]) ? $config[$configName] : null; - } - - /** - * Gets the default settings in the actual Javascript object so that - * the config JSON doesn't get polluted with default settings - * - * @return array - */ - 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"); - } - - return Convert::json2array(file_get_contents($file_path)); - } - - /** - * Gets the thumbnail width given the current view type - * - * @return int - */ - public function getSelectedThumbnailWidth() - { - if($w = $this->getSetting('thumbnailWidth')) { - return $w; - } - - $setting = $this->view == "grid" ? 'grid_thumbnail_width' : 'list_thumbnail_width'; - - return $this->config()->$setting; - } - - /** - * Gets the thumbnail height given the current view type - * - * @return int - */ - public function getSelectedThumbnailHeight() - { - if($h = $this->getSetting('thumbnailHeight')) { - return $h; - } - - $setting = $this->view == "grid" ? 'grid_thumbnail_height' : 'list_thumbnail_height'; - - return $this->config()->$setting; - } - - /** - * Creates a JSON representation of the settings. Augments the list with various - * parameters calculated at run time. - * - * @return string - */ - public function getConfigJSON() - { - $data = $this->settings; - $defaults = $this->getDefaults(); - 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])) { - 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)) { - $data[$js_name] = $value; - } - } - - $data['params'] = ($this->params) ? $this->params : null; - $data['thumbnailsDir'] = $this->ThumbnailsDir(); - $data['thumbnailWidth'] = $this->getSelectedThumbnailWidth(); - $data['thumbnailHeight'] = $this->getSelectedThumbnailHeight(); - - if(!$this->IsMultiple()) { - $data['maxFiles'] = 1; - } - - if($this->isCMS()) { - $data['urlSelectDialog'] = $this->Link('select'); - 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; - } + + /** + * The allowed actions for the RequestHandler + * @var array + */ + private static $allowed_actions = [ + 'upload', + 'handleSelect', + ]; + + + private static $url_handlers = [ + 'select' => 'handleSelect', + ]; + + /** + * Track files that are uploaded and remove the tracked files when + * they are saved into a record. + * + * @var boolean + */ + private static $track_files = false; + + /** + * 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; + + /** + * A list of custom permissions for this instance + * Options available: + * - upload + * - 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'; + + /** + * 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\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); + } + + /** + * Translate camelCase to underscore_case + * @param string $str + * @return string + */ + public static function underscorise($str) + { + $str[0] = strtolower($str[0]); + + return preg_replace_callback('/([A-Z])/', function ($c) { + return "_" . strtolower($c[1]); + }, $str); + } + + /** + * Looks at the php.ini and takes the lower of two values, translates it into + * an int representing the number of bytes allowed per upload + * + * @return int + */ + public static function get_filesize_from_ini() + { + $bytes = min([ + File::ini2bytes(ini_get('post_max_size') ?: '8M'), + File::ini2bytes(ini_get('upload_max_filesize') ?: '2M') + ]); + + return floor($bytes / (1024 * 1024)); + } + + /** + * Constructor. Sets some default permissions + * @param string $name + * @param string $title + * @param string $value + * @param Form $form + */ + public function __construct($name, $title = null, $value = null, $form = null) + { + $instance = $this; + + $this->permissions['upload'] = true; + $this->permissions['detach'] = true; + $this->permissions['delete'] = function () use ($instance) { + return Injector::inst()->get(File::class)->canDelete() && $instance->isCMS(); + }; + $this->permissions['attach'] = function () use ($instance) { + return $instance->isCMS(); + }; + + parent::__construct($name, $title, $value, $form); + } + + /** + * Renders the form field, loads requirements. Sets file size based on php.ini + * Adds the security token + * + * @param array $attributes + * @return SSViewer + */ + public function FieldHolder($attributes = []) + { + $this->defineFieldHolderRequirements(); + return parent::FieldHolder($attributes); + } + + /** + * Renders the small form field holder, loads requirements. Sets file size based on php.ini + * Adds the security token + * + * @param array $attributes + * @return SSViewer + */ + public function SmallFieldHolder($attributes = []) + { + $this->defineFieldHolderRequirements(); + return parent::SmallFieldHolder($attributes); + } + + /** + * Define some requirements and settings just before rendering the Field Holder. + */ + protected function defineFieldHolderRequirements() + { + 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('resources/vendor/unclecheese/dropzone/css/file_attachment_field.css'); + + if (!$this->getSetting('url')) { + $this->settings['url'] = $this->Link('upload'); + } + + 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 + // is attached to a record that has a multi relation, set that automatically. + $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) { + $this->imagesOnly(); + } + } + } + + /** + * Saves the field into a record + * @param DataObjectInterface $record + * @return FileAttachmentField + */ + public function saveInto(DataObjectInterface $record) + { + $fieldname = $this->getName(); + 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()); + + if ($deletions) { + foreach ($deletions as $id) { + $this->deleteFileByID($id); + } + } + + 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)) { + $record->$fieldname = is_array($this->Value()) ? implode(',', $this->Value()) : $this->Value(); + } + + if ($this->getTrackFiles()) { + $fileIDs = (array)$this->Value(); + FileAttachmentFieldTrack::untrack($fileIDs); + } + + return $this; + } + + /** + * Set the form method, e.g. PUT + * @param string $method + * @return FileAttachmentField + */ + public function setMethod($method) + { + $this->settings['method'] = $method; + + return $this; + } + + /** + * Return whether files are tracked or not. + * @return boolean + */ + public function getTrackFiles() + { + if (isset($this->settings['trackFiles']) && $this->settings['trackFiles'] !== null) { + return $this->settings['trackFiles']; + } + return $this->config()->track_files; + } + + /** + * Enable/disable file tracking on uploads + * @param boolean $bool + * @return FileAttachmentField + */ + public function setTrackFiles($bool) + { + $this->settings['trackFiles'] = $bool; + return $this; + } + + /** + * Sets number of allowed parallel uploads + * @param int $num + * @return FileAttachmentField + */ + public function setParallelUploads($num) + { + $this->settings['parallelUploads'] = $num; + + return $this; + } + + /** + * Allow multiple files + * @param boolean $bool + * @return FileAttachmentField + */ + public function setMultiple($bool) + { + $this->settings['uploadMultiple'] = $bool; + + return $this; + } + + /** + * Max filesize for uploads, in megabytes. + * Defaults to upload_max_filesize + * @param string $num + * @return FileAttachmentField + */ + public function setMaxFilesize($num) + { + $this->settings['maxFilesize'] = $num; + $validator = $this->getValidator(); + if ($validator) { + $validator->setAllowedMaxFileSize($num . 'm'); + } + return $this; + } + + /** + * Maximum number of files allowed to be attached + * @param int $num + * @return $this + */ + public function setMaxFiles($num) + { + $this->settings['maxFiles'] = $num; + + return $this; + } + + /** + * Maximum number of files allowed to be attached + * (Keeps API consistent with UploadField) + * @param int $num + * @return $this + */ + public function setAllowedMaxFileNumber($num) + { + return $this->setMaxFiles($num); + } + + /** + * Sets the name of the upload parameter, e.g. "Files" + * @param string $name + * @return FileAttachmentField + */ + public function setParamName($name) + { + $this->settings['paramName'] = $name; + + return $this; + } + + /** + * Allow or disallow image thumbnails created client side + * @param boolean $bool + * @return FileAttachmentField + */ + public function setCreateImageThumbnails($bool) + { + $this->settings['createImageThumbnails'] = $bool; + + return $this; + } + + /** + * Set the threshold at which to not create an image thumbnail + * @param int $num + * @return FileAttachmentField + */ + public function setMaxThumbnailFilesize($num) + { + $this->settings['thumbnailFilesize'] = $num; + + return $this; + } + + /** + * Add an array of IDs + * + * @return void + */ + public function addValidFileIDs(array $ids) + { + $request = Injector::inst()->get(HTTPRequest::class); + $session = $request->getSession(); + + $validIDs = $session->get('FileAttachmentField.validFileIDs'); + if (!$validIDs) { + $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. + * + * @return array + */ + public function getValidFileIDs() + { + $request = Injector::inst()->get(HTTPRequest::class); + $session = $request->getSession(); + $validIDs = $session->get('FileAttachmentField.validFileIDs'); + if ($validIDs && is_array($validIDs)) { + return $validIDs; + } + return []; + } + + /** + * Check that the user is submitting the file IDs that they uploaded. + * + * @return boolean + */ + public function validate($validator) + { + $result = true; + + // 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' + // (Below validation isn't triggered as setValue() removes the invalid ID + // to prevent the CMS from loading something it shouldn't, also stops the + // validator from realizing there's an invalid ID.) + $validator->validationError( + $this->name, + _t( + 'FileAttachmentField.VALIDATION', + 'Invalid file ID sent.' + ), + "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.', + ['id' => $id] + ), + "validation" + ); + } + $result = false; + } + } + } + } + + return $result; + } + + /** + * @param int|array $val + * @param array|DataObject $data + * @return $this + */ + 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 = []; + if ($data->getSchema()->hasOneComponent($data->getClassName(), $fieldName)) { + $id = $data->{$fieldName . 'ID'}; + if ($id) { + $ids[] = $id; + } + } 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; + } + } + } + } + if ($ids) { + $this->addValidFileIDs($ids); + } + } + if ($data && is_array($data) && isset($data[$this->getName()])) { + // Prevent Form::loadDataFrom() from loading invalid File IDs + // that may have been passed. + $isInvalid = false; + $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) { + if ($validIDs && !isset($validIDs[$id])) { + unset($ids[$i]); + $isInvalid = true; + } + } + if ($isInvalid) { + $ids = array_values($ids); + $val = $ids; + $this->hasInvalidFileID = true; + } + unset($ids); // stop $ids variable from modifying to $data array. + } + } + return parent::setValue($val, $data); + } + + /** + * The thumbnail width + * @param int $num + * @return FileAttachmentField + */ + public function setThumbnailWidth($num) + { + $this->settings['thumbnailWidth'] = $num; + + return $this; + } + + /** + * The thumbnail height + * @param int $num + * @return FileAttachmentField + */ + public function setThumbnailHeight($num) + { + $this->settings['thumbnailHeight'] = $num; + + return $this; + } + + /** + * The layout of the uploader, either "grid" or "list" + * @param string $view + * @return FileAttachmentField + */ + public function setView($view) + { + if (!in_array($view, ['grid', 'list'])) { + throw new Exception("FileAttachmentField::setView - View must be one of 'grid' or 'list'"); + } + + $this->view = $view; + + return $this; + } + + /** + * Gets the current view + * @return string + */ + public function getView() + { + return $this->view; + } + + /** + * Set the selector for the clickable element. Use a boolean for the + * entire dropzone. + * @param string|bool $val + * @return FileAttachmentField + */ + public function setClickable($val) + { + $this->settings['clickable'] = $val; + + return $this; + } + + /** + * A list of accepted file extensions + * @param array $files + * @return FileAttachmentField + */ + public function setAcceptedFiles($files = []) + { + if (is_array($files)) { + $files = implode(',', $files); + } + $files = str_replace(' ', '', $files); + $this->settings['acceptedFiles'] = $files; + + // Update validator + $validator = $this->getValidator(); + if ($validator) { + $fileExts = explode(',', $files); + + $validatorExts = []; + foreach ($fileExts as $fileExt) { + if ($fileExt && isset($fileExt[0]) && $fileExt[0] === '.') { + $fileExt = substr($fileExt, 1); + } + $validatorExts[] = $fileExt; + } + $validator->setAllowedExtensions($validatorExts); + } + + return $this; + } + + /** + * A helper method to only allow images files + * @return FileAttachmentField + */ + public function imagesOnly() + { + $this->setAcceptedFiles(['.png', '.gif', '.jpeg', '.jpg']); + + return $this; + } + + /** + * Sets the allowed mime types + * @param array $types + * @return FileAttachmentField + */ + public function setAcceptedMimeTypes($types = []) + { + if (is_array($types)) { + $types = implode(',', $types); + } + $this->settings['acceptedMimeTypes'] = $types; + + return $this; + } + + /** + * Set auto-processing. If true, uploads happen on addition to the queue + * @param boolean $bool + * @return FileAttachmentField + */ + public function setAutoProcessQueue($bool) + { + $this->settings['autoProcessQueue'] = $bool; + + return $this; + } + + /** + * Set the selector for the container element that holds all of the + * uploaded files + * @param string $val + * @return FileAttachmentField + */ + public function setPreviewsContainer($val) + { + $this->settings['previewsContainer'] = $val; + + return $this; + } + + /** + * Sets the max resolution for images, in pixels + * @param int $pixels + */ + public function setMaxResolution($pixels) + { + $this->settings['maxResolution'] = $pixels; + + return $this; + } + + /** + * Sets the min resolution for images, in pixels + * @param int $pixels + */ + public function setMinResolution($pixels) + { + $this->settings['minResolution'] = $pixels; + return $this; + } + + /** + * Sets selector for the preview template + * @param string $template + * @return FileAttachmentField + */ + public function setPreviewTemplate($template) + { + $this->previewTemplate = $template; + + return $this; + } + + /** + * Adds an arbitrary key/val params to send to the server with the upload + * @param string $key + * @param mixed $val + * @return FileAttachmentField + */ + public function addParam($key, $val) + { + $this->params[$key] = $val; + + return $this; + } + + /** + * Sets permissions for this uploader: "detach", "upload", "delete", "attach" + * Permissions can be boolean or Callable + * @param array $perms + * @return FileAttachmentField + */ + public function setPermissions($perms) + { + foreach ($perms as $perm => $val) { + if (!isset($this->permissions[$perm])) { + throw new Exception("FileAttachmentField::setPermissions - Permission $perm is not allowed"); + } + $this->permissions[$perm] = $val; + } + + return $this; + } + + /** + * 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 + */ + public function setPermission($perm, $val) + { + return $this->setPermissions([ + $perm => $val + ]); + } + + /** + * @param String + */ + public function setDisplayFolderName($name) + { + $this->displayFolderName = $name; + return $this; + } + + /** + * @return String + */ + public function getDisplayFolderName() + { + return $this->displayFolderName; + } + + /** + * 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 + * @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; + } + 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 + * post_max_size - requests over this size will cause the $_FILES array to be empty + * 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 + * @return HTTPResponse + * @return HTTPResponse + */ + public function upload(HTTPRequest $request) + { + + $name = $this->getSetting('paramName'); + $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.'); + 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.'); + 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) { + $token = $form->getSecurityToken(); + 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 = []; + foreach (['name', 'type', 'tmp_name', 'error', 'size'] as $field) { + $tmpFile[$field] = $files[$field][$i]; + } + $tmpFiles[] = $tmpFile; + } + } elseif (!empty($files['tmp_name'])) { + $tmpFiles[] = $files; + } + + $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 = 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.'); + return $this->httpError(400, $error_message); + } + + if ($this->upload->isError()) { + return $this->httpError(400, implode(' ' . PHP_EOL, $this->upload->getErrors())); + } + + if ($this->getTrackFiles()) { + $controller = Controller::has_curr() ? Controller::curr() : null; + $formClass = ($form) ? get_class($form) : ''; + + $trackFile = FileAttachmentFieldTrack::create(); + if ($controller instanceof LeftAndMain) { + // If in CMS (store DataObject or Page) + $formController = $form->getController(); + $trackFile->ControllerClass = $formController->class; + if (!$formController instanceof LeftAndMain) { + $trackFile->setRecord($formController->getRecord()); + } + } else { + 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(); + } + } + + $this->addValidFileIDs($ids); + $this->extend('onAfterUploadFiles', $ids); + return new HTTPResponse(implode(',', $ids), 200); + } + + /** + * @param HTTPRequest $request + * @return UploadField_ItemHandler + */ + public function handleSelect(HTTPRequest $request) + { + if ($this->isDisabled() || $this->isReadonly() || !$this->CanAttach()) { + return $this->httpError(403); + } + + return FileAttachmentField_SelectHandler::create($this, $this->getFolderName()); + } + + /** + * Deletes a file. Ensures user has permissions and the file is part + * of the current record, so as not to allow arbitrary deletion of files + * + * @param int $id + * @return boolean + */ + protected function deleteFileByID($id) + { + if ($this->CanDelete() && $record = $this->getRecord()) { + if ($relation = $this->getRelation()) { + $file = $relation->byID($id); + } else { + if ($record->has_one($this->getName())) { + $file = $record->{$this->getName()}(); + } + } + + if ($file && $file->canDelete()) { + $file->delete(); + + return true; + } + } + + return false; + } + + /** + * A template accessor that determines if the uploader is in "multiple" mode + * + * @return boolean + */ + public function IsMultiple() + { + if ($this->getSetting('uploadMultiple')) { + return true; + } + + if ($record = $this->getRecord()) { + return ($record->manyMany($this->getName()) || $record->hasMany($this->getName())); + } + + return false; + } + + /** + * The name of the input, e.g. the "has_one" or "many_many" relation name + * + * @return string + */ + public function InputName() + { + return $this->IsMultiple() ? $this->getName() . "[]" : $this->getName(); + } + + /** + * Gets a list of all the files that are attached to the record + * + * @return SS_List + */ + public function AttachedFiles() + { + if ($record = $this->getRecord()) { + if ($record->hasMethod($this->getName())) { + $result = $record->{$this->getName()}(); + if ($result instanceof SS_List) { + return $result; + } else { + if ($result->exists()) { + return ArrayList::create([$result]); + } + } + } + } + + if ($ids = $this->dataValue()) { + if ($ids instanceof ManyManyList) { + $ids = array_keys($ids->map()->toArray()); + } + + if (!is_array($ids)) { + $ids = explode(',', $ids); + } + + $attachments = ArrayList::create(); + foreach ($ids as $id) { + $file = File::get()->byID((int)$id); + if ($file && $file->canView()) { + $attachments->push($file); + } + } + return $attachments; + } + + return false; + } + + /** + * Gets the directory that contains all the file icons organised into sizes + * + * @return string + */ + public function RootThumbnailsDir() + { + 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 + */ + public function ThumbnailsDir() + { + return $this->RootThumbnailsDir() . '/' . $this->TemplateThumbnailSize() . "px"; + } + + public function CSSSize() + { + $w = $this->getSelectedThumbnailWidth(); + if ($w < 150) { + return "small"; + } + if ($w < 250) { + return "medium"; + } + + return "large"; + } + + /** + * The directory that the module is installed to. A template accessor + * + * @return string + */ + public function DropzoneDir() + { + return basename(dirname(__FILE__)); + } + + /** + * Gets the value + * + * @return string|array + */ + public function Value() + { + return $this->dataValue(); + } + + /** + * Returns true if the "upload" permission returns true + * + * @return boolean + */ + public function CanUpload() + { + return $this->checkPerm('upload'); + } + + /** + * Returns true if the "delete" permission returns true + * + * @return boolean + */ + public function CanDelete() + { + return $this->checkPerm('delete'); + } + + /** + * Returns true if the "detach" permission returns true + * + * @return boolean + */ + public function CanDetach() + { + return $this->checkPerm('detach'); + } + + /** + * Returns true if the "attach" permission returns true + * + * @return boolean + */ + public function CanAttach() + { + return $this->checkPerm('attach'); + } + + /** + * Renders the preview template, optionally for a given file + * @param int $fileID + */ + public function PreviewTemplate($fileID = null) + { + return $this->renderWith($this->previewTemplate); + + } + + /** + * Gets the closest thumbnail size for the template, given the list of + * icon_sizes (e.g. 32px, 64px, 128px) + * + * @return int + */ + public function TemplateThumbnailSize() + { + $w = $this->getSelectedThumbnailWidth(); + + foreach ($this->config()->icon_sizes as $size) { + if ($w <= $size) { + return $size; + } + } + } + + /** + * Returns true if the uploader auto-processes + * + * @return boolean + */ + public function AutoProcess() + { + $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 (is_callable($this->permissions[$perm])) { + return $this->permissions[$perm](); + } + + return $this->permissions[$perm]; + } + + /** + * Gets the classname for the file, e.g. from the declared + * file relation on the record. + * + * If given a filename, look at the extension and upgrade it + * to an Image if necessary. + * + * @param string $filename + * @return string + */ + public function getFileClass($filename = null) + { + $name = $this->getName(); + $record = $this->getRecord(); + + $ext = pathinfo($filename ?? '', PATHINFO_EXTENSION); + $defaultClass = File::get_class_for_file_extension($ext); + + if (empty($name) || empty($record)) { + return $defaultClass; + } + + if ($record) { + $class = $record->getRelationClass($name); + if (!$class) { + $class = File::class; + } + } + + if ($filename) { + if ($defaultClass == Image::class && + $this->config()->upgrade_images && + !Injector::inst()->get($class) instanceof Image + ) { + $class = Image::class; + } + } + + return $class; + } + + /** + * Get the record that this form field is editing + * @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; + } + + /** + * 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) { + $fieldname = $this->getName(); + $relation = $record->hasMethod($fieldname) ? $record->$fieldname() : null; + + return ($relation && ($relation instanceof RelationList || $relation instanceof UnsavedRelationList)) ? $relation : false; + } + + return false; + } + + /** + * Gets a given setting. Falls back on Config defaults + * + * Note: config settings are in underscore_case + * + * @param string $setting + * @return mixed + */ + protected function getSetting($setting) + { + if (isset($this->settings[$setting])) { + return $this->settings[$setting]; + } + + $config = Config::inst()->get(__CLASS__, "defaults"); + $configName = static::underscorise($setting); + + return isset($config[$configName]) ? $config[$configName] : null; + } + + /** + * Gets the default settings in the actual Javascript object so that + * the config JSON doesn't get polluted with default settings + * + * @return array + */ + protected function getDefaults() + { + $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)); + } + + /** + * Gets the thumbnail width given the current view type + * @return int + */ + public function getSelectedThumbnailWidth() + { + if ($w = $this->getSetting('thumbnailWidth')) { + return $w; + } + + $setting = $this->view == "grid" ? 'grid_thumbnail_width' : 'list_thumbnail_width'; + + return $this->config()->$setting; + } + + /** + * Gets the thumbnail height given the current view type + * @return int + */ + public function getSelectedThumbnailHeight() + { + if ($h = $this->getSetting('thumbnailHeight')) { + return $h; + } + + $setting = $this->view == "grid" ? 'grid_thumbnail_height' : 'list_thumbnail_height'; + + return $this->config()->$setting; + } + + /** + * Creates a JSON representation of the settings. Augments the list with various + * parameters calculated at run time. + * + * @return string + */ + public function getConfigJSON() + { + $data = $this->settings; + $defaults = $this->getDefaults(); + 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])) { + 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)) { + $data[$js_name] = $value; + } + } + + $data['params'] = ($this->params) ? $this->params : null; + $data['thumbnailsDir'] = $this->ThumbnailsDir(); + $data['thumbnailWidth'] = $this->getSelectedThumbnailWidth(); + $data['thumbnailHeight'] = $this->getSelectedThumbnailHeight(); + + if (!$this->IsMultiple()) { + $data['maxFiles'] = 1; + } + + if ($this->isCMS()) { + $data['urlSelectDialog'] = $this->Link('select'); + if ($this->getFolderName()) { + $data['folderID'] = Folder::find_or_make($this->getFolderName())->ID; + } + } + + return Convert::array2json($data); + } } diff --git a/src/FileAttachmentField_SelectHandler.php b/src/FileAttachmentField_SelectHandler.php new file mode 100644 index 0000000..c95980f --- /dev/null +++ b/src/FileAttachmentField_SelectHandler.php @@ -0,0 +1,93 @@ +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..cd3e12c 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..495b91e 100644 --- a/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss +++ b/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss @@ -1,59 +1,58 @@ -
  • -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 + $File.Title + + <%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 %> <% if $Scope.CanDelete %> - +
    <%t Dropzone.DELETED 'deleted' %>
    <%t Dropzone.CHANGEAFTERSAVE 'The change will take effect after you save.' %> - +
    <% end_if %> From cc490d1dabdf9fbf36a9668733055d3fc9928b74 Mon Sep 17 00:00:00 2001 From: Pack Date: Wed, 11 Sep 2024 15:18:16 +1200 Subject: [PATCH 2/7] removed extra line that not necessary --- src/FileAttachmentField_SelectHandler.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/FileAttachmentField_SelectHandler.php b/src/FileAttachmentField_SelectHandler.php index c95980f..57a631f 100644 --- a/src/FileAttachmentField_SelectHandler.php +++ b/src/FileAttachmentField_SelectHandler.php @@ -2,7 +2,6 @@ namespace UncleCheese\DropZone; - use Embed\Adapters\File; use SilverStripe\Assets\Folder; use SilverStripe\Control\HTTPRequest; @@ -26,10 +25,11 @@ class FileAttachmentField_SelectHandler { ); /** - * @param $folderID The ID of the folder to display. + * @param $folderID - the ID of the folder to display. * @return FormField */ - protected function getListField($folderID) { + protected function getListField($folderID) + { // Generate the folder selection field. $folderField = new TreeDropdownField('ParentID', _t('HtmlEditorField.FOLDER', Folder::class), Folder::class); $folderField->setValue($folderID); @@ -66,7 +66,8 @@ protected function getListField($folderID) { return $selectComposite; } - public function filesbyid(HTTPRequest $r) { + public function filesbyid(HTTPRequest $r) + { $ids = $r->getVar('ids'); $files = File::get()->byIDs(explode(',',$ids)); From 18f712f12837d48813f78d97bce49bc7c20092d2 Mon Sep 17 00:00:00 2001 From: Pack Date: Thu, 12 Sep 2024 09:29:22 +1200 Subject: [PATCH 3/7] upgrade and compatible with CMS 5 --- composer.json | 2 +- src/DropzoneFile.php | 1 - src/FileAttachmentField.php | 10 +++++----- .../UncleCheese/Dropzone/FileAttachmentField_holder.ss | 3 --- .../Dropzone/FileAttachmentField_holder_small.ss | 2 -- .../Dropzone/FileAttachmentField_preview.ss | 6 +++--- .../Includes/FileAttachmentField_attachments.ss | 10 ++++------ 7 files changed, 13 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 72a895b..6b9c1cb 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { - "silverstripe/framework": "4.*" + "silverstripe/framework": "5" }, "autoload": { "psr-4": { diff --git a/src/DropzoneFile.php b/src/DropzoneFile.php index f38cd8c..a9f7e3e 100644 --- a/src/DropzoneFile.php +++ b/src/DropzoneFile.php @@ -69,7 +69,6 @@ public function getPreviewThumbnail($w = null, $h = null) } } - /** * Gets a filename based on the extension and the size * diff --git a/src/FileAttachmentField.php b/src/FileAttachmentField.php index e85ab4c..09aa01d 100644 --- a/src/FileAttachmentField.php +++ b/src/FileAttachmentField.php @@ -218,12 +218,12 @@ public function SmallFieldHolder($attributes = []) */ protected function defineFieldHolderRequirements() { - Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/dropzone.js'); - Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/file_attachment_field.js'); + Requirements::javascript('unclecheese/dropzone:javascript/dropzone.js'); + Requirements::javascript('unclecheese/dropzone:javascript/file_attachment_field.js'); if ($this->isCMS()) { - Requirements::javascript('resources/vendor/unclecheese/dropzone/javascript/file_attachment_field_backend.js'); + Requirements::javascript('unclecheese/dropzone:javascript/file_attachment_field_backend.js'); } - Requirements::css('resources/vendor/unclecheese/dropzone/css/file_attachment_field.css'); + Requirements::css('unclecheese/dropzone:css/file_attachment_field.css'); if (!$this->getSetting('url')) { $this->settings['url'] = $this->Link('upload'); @@ -1105,7 +1105,7 @@ public function AttachedFiles() */ public function RootThumbnailsDir() { - return $this->getSetting('thumbnailsDir') ?: 'resources/vendor/unclecheese/dropzone/images/file-icons'; + return $this->getSetting('thumbnailsDir') ?: '_resources/vendor/unclecheese/dropzone/images/file-icons'; } /** diff --git a/templates/UncleCheese/Dropzone/FileAttachmentField_holder.ss b/templates/UncleCheese/Dropzone/FileAttachmentField_holder.ss index d82872b..3764a32 100644 --- a/templates/UncleCheese/Dropzone/FileAttachmentField_holder.ss +++ b/templates/UncleCheese/Dropzone/FileAttachmentField_holder.ss @@ -27,8 +27,6 @@ <% end_if %> - - @@ -44,7 +42,6 @@ <% if not $AutoProcess %> <% end_if %> -
    diff --git a/templates/UncleCheese/Dropzone/FileAttachmentField_holder_small.ss b/templates/UncleCheese/Dropzone/FileAttachmentField_holder_small.ss index 9b70401..a177cfc 100644 --- a/templates/UncleCheese/Dropzone/FileAttachmentField_holder_small.ss +++ b/templates/UncleCheese/Dropzone/FileAttachmentField_holder_small.ss @@ -25,8 +25,6 @@ <% end_if %> - - diff --git a/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss b/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss index cd3e12c..df9db0a 100644 --- a/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss +++ b/templates/UncleCheese/Dropzone/FileAttachmentField_preview.ss @@ -1,7 +1,7 @@
  • - + @@ -18,13 +18,13 @@ - +
    <%t Dropzone.ERROR 'Oh no!' %>
    - +
  • diff --git a/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss b/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss index 495b91e..675c46c 100644 --- a/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss +++ b/templates/UncleCheese/Dropzone/Includes/FileAttachmentField_attachments.ss @@ -4,7 +4,6 @@ <% if $File.Orientation > -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 %> @@ -28,13 +27,13 @@ <% if $Scope.CanDetach %> <%t Dropzone.DETACHFILE 'remove' %> - + <% end_if %> <% if $Scope.CanDelete %> <%t Dropzone.MARKFORDELETION 'delete' %> - + <% end_if %> @@ -43,7 +42,7 @@
    <%t Dropzone.REMOVED 'removed' %>
    <%t Dropzone.CHANGEAFTERSAVE 'The change will take effect after you save.' %> - +
    <% end_if %> @@ -52,9 +51,8 @@
    <%t Dropzone.DELETED 'deleted' %>
    <%t Dropzone.CHANGEAFTERSAVE 'The change will take effect after you save.' %> - +
    <% end_if %> - From 0cc055d27ca5ddc067de6ab0b3a4de8b6cccbda1 Mon Sep 17 00:00:00 2001 From: Pack Date: Thu, 12 Sep 2024 15:57:08 +1200 Subject: [PATCH 4/7] change silverstripe/framework any above version 5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6b9c1cb..49eadb5 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { - "silverstripe/framework": "5" + "silverstripe/framework": "^4 || ^5" }, "autoload": { "psr-4": { From 32459d62325ff671525cca1180394cf25234d164 Mon Sep 17 00:00:00 2001 From: Pack Date: Mon, 16 Sep 2024 16:13:07 +1200 Subject: [PATCH 5/7] methods changed in CMS 5 --- src/FileAttachmentField.php | 10 +++++----- ...andler.php => FileAttachmentFieldSelectHandler.php} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/{FileAttachmentField_SelectHandler.php => FileAttachmentFieldSelectHandler.php} (98%) diff --git a/src/FileAttachmentField.php b/src/FileAttachmentField.php index 09aa01d..06e3dc9 100644 --- a/src/FileAttachmentField.php +++ b/src/FileAttachmentField.php @@ -157,8 +157,8 @@ public static function underscorise($str) public static function get_filesize_from_ini() { $bytes = min([ - File::ini2bytes(ini_get('post_max_size') ?: '8M'), - File::ini2bytes(ini_get('upload_max_filesize') ?: '2M') + Convert::memstring2bytes(('post_max_size') ?: '8M'), + Convert::memstring2bytes(('upload_max_filesize') ?: '2M') ]); return floor($bytes / (1024 * 1024)); @@ -997,7 +997,7 @@ public function handleSelect(HTTPRequest $request) return $this->httpError(403); } - return FileAttachmentField_SelectHandler::create($this, $this->getFolderName()); + return FileAttachmentFieldSelectHandler::create($this, $this->getFolderName()); } /** @@ -1363,7 +1363,7 @@ protected function getDefaults() throw new \Exception("FileAttachmentField::getDefaults() - There is no config json file at $file_path"); } - return Convert::json2array(file_get_contents($file_path)); + return json_decode(file_get_contents($file_path), true); } /** @@ -1436,6 +1436,6 @@ public function getConfigJSON() } } - return Convert::array2json($data); + return json_encode($data); } } diff --git a/src/FileAttachmentField_SelectHandler.php b/src/FileAttachmentFieldSelectHandler.php similarity index 98% rename from src/FileAttachmentField_SelectHandler.php rename to src/FileAttachmentFieldSelectHandler.php index 57a631f..be900f6 100644 --- a/src/FileAttachmentField_SelectHandler.php +++ b/src/FileAttachmentFieldSelectHandler.php @@ -18,7 +18,7 @@ use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; -class FileAttachmentField_SelectHandler { +class FileAttachmentFieldSelectHandler { private static $allowed_actions = array ( 'filesbyid', From 4caa1e682b607897960e5eaff1df809268e13704 Mon Sep 17 00:00:00 2001 From: Pack Date: Fri, 20 Sep 2024 10:52:10 +1200 Subject: [PATCH 6/7] fix maxFilesize get overridden --- javascript/dropzone.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/javascript/dropzone.js b/javascript/dropzone.js index 7b4e76d..149f70b 100644 --- a/javascript/dropzone.js +++ b/javascript/dropzone.js @@ -279,9 +279,9 @@ require.register("dropzone/lib/dropzone.js", function (exports, module) { /* This is a list of all available events you can register on a dropzone object. - + You can register an event handler like this: - + dropzone.on("dragEnter", function() { }); */ @@ -420,7 +420,7 @@ require.register("dropzone/lib/dropzone.js", function (exports, module) { this.element.classList.add("dz-started"); } if (this.previewsContainer) { - + file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim()); file.previewTemplate = file.previewElement; this.previewsContainer.appendChild(file.previewElement); @@ -585,7 +585,8 @@ require.register("dropzone/lib/dropzone.js", function (exports, module) { Dropzone.instances.push(this); this.element.dropzone = this; elementOptions = (_ref = Dropzone.optionsForElement(this.element)) != null ? _ref : {}; - this.options = extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); + this.options = extend({}, elementOptions, this.defaultOptions, options != null ? options : {}); + this.option.maxFilesize = 256; if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { return this.options.fallback.call(this); } @@ -1752,7 +1753,7 @@ require.register("dropzone/lib/dropzone.js", function (exports, module) { /* - + Bugfix for iOS 6 and 7 Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios based on the work of https://github.com/stomita/ios-imagefile-megapixel @@ -1872,4 +1873,4 @@ if (typeof exports == "object") { } else { this["Dropzone"] = require("dropzone"); } -})() \ No newline at end of file +})() From 1b72b6c537980073ef12fe1b8ce7033cffee9b7c Mon Sep 17 00:00:00 2001 From: Pack Date: Fri, 20 Sep 2024 11:02:20 +1200 Subject: [PATCH 7/7] maxFilesize adjust logic --- javascript/dropzone.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/javascript/dropzone.js b/javascript/dropzone.js index 149f70b..9251d10 100644 --- a/javascript/dropzone.js +++ b/javascript/dropzone.js @@ -585,8 +585,10 @@ require.register("dropzone/lib/dropzone.js", function (exports, module) { Dropzone.instances.push(this); this.element.dropzone = this; elementOptions = (_ref = Dropzone.optionsForElement(this.element)) != null ? _ref : {}; - this.options = extend({}, elementOptions, this.defaultOptions, options != null ? options : {}); - this.option.maxFilesize = 256; + this.options = extend({}, this.defaultOptions, elementOptions, options != null ? options : {}); + if (this.options.maxFilesize === 0) { + this.options.maxFilesize = 256; + } if (this.options.forceFallback || !Dropzone.isBrowserSupported()) { return this.options.fallback.call(this); }