diff --git a/code/Control/UserDefinedFormController.php b/code/Control/UserDefinedFormController.php index be8df357..4fd9d7c0 100644 --- a/code/Control/UserDefinedFormController.php +++ b/code/Control/UserDefinedFormController.php @@ -313,49 +313,74 @@ public function process($data, $form) $field->createProtectedFolder(); } - $file = Versioned::withVersionedMode(function () use ($field, $form) { - $stage = Injector::inst()->get(UserDefinedFormController::class)->config()->get('file_upload_stage'); - Versioned::set_stage($stage); - - $foldername = $field->getFormField()->getFolderName(); - // create the file from post data - $upload = Upload::create(); - try { - $upload->loadIntoFile($_FILES[$field->Name], null, $foldername); - } catch (ValidationException $e) { - $validationResult = $e->getResult(); - foreach ($validationResult->getMessages() as $message) { - $form->sessionMessage($message['message'], ValidationResult::TYPE_ERROR); + $processFile = function ($fileData) use ($field, $form, &$attachments) { + $file = Versioned::withVersionedMode(function () use ($fileData, $field, $form, &$attachments) { + $stage = Injector::inst()->get(UserDefinedFormController::class)->config()->get('file_upload_stage'); + Versioned::set_stage($stage); + + $foldername = $field->getFormField()->getFolderName(); + // create the file from post data + $upload = Upload::create(); + try { + $upload->loadIntoFile($fileData, null, $foldername); + } catch (ValidationException $e) { + $validationResult = $e->getResult(); + foreach ($validationResult->getMessages() as $message) { + $form->sessionMessage($message['message'], ValidationResult::TYPE_ERROR); + } + Controller::curr()->redirectBack(); + return null; } - Controller::curr()->redirectBack(); + /** @var AssetContainer|File $file */ + $file = $upload->getFile(); + $file->ShowInSearch = 0; + $file->UserFormUpload = UserFormFileExtension::USER_FORM_UPLOAD_TRUE; + $file->write(); + + return $file; + }); + + if (is_null($file)) { return null; } - /** @var AssetContainer|File $file */ - $file = $upload->getFile(); - $file->ShowInSearch = 0; - $file->UserFormUpload = UserFormFileExtension::USER_FORM_UPLOAD_TRUE; - $file->write(); + + // generate image thumbnail to show in asset-admin + // you can run userforms without asset-admin, so need to ensure asset-admin is installed + if (class_exists(AssetAdmin::class)) { + AssetAdmin::singleton()->generateThumbnails($file); + } + + // attach a file to recipient email only if lower than configured size + if ($file->getAbsoluteSize() <= $this->getMaximumAllowedEmailAttachmentSize()) { + $attachments[$field->Name][] = $file; + } return $file; - }); + }; - if (is_null($file)) { - return; - } + $files = $_FILES[$field->Name]; - // generate image thumbnail to show in asset-admin - // you can run userforms without asset-admin, so need to ensure asset-admin is installed - if (class_exists(AssetAdmin::class)) { - AssetAdmin::singleton()->generateThumbnails($file); - } + if (is_array($files['name'])) { + foreach (array_keys((array) $files['name']) as $key) { + $fileData = []; + foreach ($files as $column => $data) { + $fileData[$column] = $data[$key]; + } - // write file to form field - $submittedField->UploadedFileID = $file->ID; + if (!$file = $processFile($fileData)) { + return; + } + + // write file to form field + $submittedField->UploadedFiles()->add($file); + } + } else { + if (!$file = $processFile($files)) { + return; + } - // attach a file to recipient email only if lower than configured size - if ($file->getAbsoluteSize() <= $this->getMaximumAllowedEmailAttachmentSize()) { - // using the field name as array index is fine as file upload field only allows one file - $attachments[$field->Name] = $file; + // write file to form field + $submittedField->UploadedFileID = $file->ID; } } } @@ -393,21 +418,23 @@ public function process($data, $form) $mergeFields = $this->getMergeFieldsMap($emailData['Fields']); if ($attachments && (bool) $recipient->HideFormData === false) { - foreach ($attachments as $uploadFieldName => $file) { - /** @var File $file */ - if ((int) $file->ID === 0) { - continue; - } + foreach ($attachments as $uploadFieldName => $files) { + foreach ($files as $file) { + /** @var File $file */ + if ((int) $file->ID === 0) { + continue; + } - $canAttachFileForRecipient = true; - $this->extend('updateCanAttachFileForRecipient', $canAttachFileForRecipient, $recipient, $uploadFieldName, $file); + $canAttachFileForRecipient = true; + $this->extend('updateCanAttachFileForRecipient', $canAttachFileForRecipient, $recipient, $uploadFieldName, $file); - if ($canAttachFileForRecipient) { - $email->addAttachmentFromData( - $file->getString(), - $file->getFilename(), - $file->getMimeType() - ); + if ($canAttachFileForRecipient) { + $email->addAttachmentFromData( + $file->getString(), + $file->getFilename(), + $file->getMimeType() + ); + } } } } diff --git a/code/Form/UserForm.php b/code/Form/UserForm.php index 106452fe..d65d1f81 100644 --- a/code/Form/UserForm.php +++ b/code/Form/UserForm.php @@ -10,6 +10,7 @@ use SilverStripe\Forms\FormAction; use SilverStripe\UserForms\FormField\UserFormsStepField; use SilverStripe\UserForms\FormField\UserFormsFieldList; +use SilverStripe\UserForms\Model\EditableFormField\EditableFileField; /** * @package userforms @@ -165,12 +166,23 @@ public function getFormActions() public function getRequiredFields() { // Generate required field validator - $requiredNames = $this + $requiredFields = $this ->getController() ->data() ->Fields() - ->filter('Required', true) - ->column('Name'); + ->filter('Required', true); + + $requiredNames = []; + foreach ($requiredFields as $requiredField) { + $requiredName = $requiredField->Name; + + if ($requiredField instanceof EditableFileField && $requiredField->Multiple) { + $requiredName .= '[]'; + } + + $requiredNames[] = $requiredName; + } + $requiredNames = array_merge($requiredNames, $this->getEmailRecipientRequiredFields()); $required = UserFormsRequiredFields::create($requiredNames); $this->extend('updateRequiredFields', $required); diff --git a/code/Form/UserFormsRequiredFields.php b/code/Form/UserFormsRequiredFields.php index e36a5822..c26920fd 100644 --- a/code/Form/UserFormsRequiredFields.php +++ b/code/Form/UserFormsRequiredFields.php @@ -57,12 +57,19 @@ public function php($data) continue; } + $originalFieldName = $fieldName; + // get form field if ($fieldName instanceof FormField) { $formField = $fieldName; $fieldName = $fieldName->getName(); + $originalFieldName = $fieldName; } else { $formField = $fields->dataFieldByName($fieldName); + + if ($formField instanceof FileField && strpos($fieldName ?? '', '[') !== false) { + $fieldName = preg_replace('#\[(.*?)\]$#', '', $fieldName ?? ''); + } } // get editable form field - owns display rules for field @@ -75,7 +82,7 @@ public function php($data) // handle error case if ($formField && $error) { - $this->handleError($formField, $fieldName); + $this->handleError($formField, $originalFieldName); $valid = false; } } @@ -122,7 +129,7 @@ private function validateRequired(FormField $field, array $data) if ($field instanceof FileField && isset($value['error']) && $value['error']) { $error = true; } else { - $error = (count($value ?? [])) ? false : true; + $error = (count(array_filter($value ?? []))) ? false : true; } } else { // assume a string or integer diff --git a/code/Model/EditableFormField/EditableFileField.php b/code/Model/EditableFormField/EditableFileField.php index 536502af..777654b2 100755 --- a/code/Model/EditableFormField/EditableFileField.php +++ b/code/Model/EditableFormField/EditableFileField.php @@ -9,6 +9,7 @@ use SilverStripe\Core\Convert; use SilverStripe\Forms\FieldList; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FileField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\NumericField; @@ -18,6 +19,7 @@ use SilverStripe\Security\InheritedPermissions; use SilverStripe\UserForms\Control\UserDefinedFormAdmin; use SilverStripe\UserForms\Control\UserDefinedFormController; +use SilverStripe\UserForms\Model\EditableCustomRule; use SilverStripe\UserForms\Model\EditableFormField; use SilverStripe\UserForms\Model\Submission\SubmittedFileField; @@ -40,6 +42,7 @@ class EditableFileField extends EditableFormField private static $db = [ 'MaxFileSizeMB' => 'Float', 'FolderConfirmed' => 'Boolean', + 'Multiple' => 'Boolean', ]; private static $has_one = [ @@ -173,6 +176,12 @@ public function getCMSFields() ->setDescription("Note: Maximum php allowed size is {$this->getPHPMaxFileSizeMB()} MB") ); + $fields->addFieldToTab( + 'Root.Main', + CheckboxField::create('Multiple') + ->setTitle('Allow multiple files') + ); + $fields->removeByName('Default'); }); @@ -209,7 +218,7 @@ public function createProtectedFolder(): void public function getFormField() { - $field = FileField::create($this->Name, $this->Title ?: false) + $field = FileField::create($this->Name . ($this->Multiple ? '[]' : ''), $this->Title ?: false) ->setFieldHolderTemplate(EditableFormField::class . '_holder') ->setTemplate(__CLASS__) ->setValidator(Injector::inst()->get(Upload_Validator::class . '.userforms', false)); @@ -231,6 +240,10 @@ public function getFormField() $field->getValidator()->setAllowedMaxFileSize(static::get_php_max_file_size()); } + if ($this->Multiple) { + $field->setAttribute('multiple', 'multiple'); + } + $folder = $this->Folder(); if ($folder && $folder->exists()) { $field->setFolderName( @@ -300,4 +313,9 @@ public function onBeforeWrite() $this->FolderConfirmed = true; } } + + public function getSelectorFieldOnly() + { + return $this->Multiple ? "[name='{$this->Name}[]']" : parent::getSelectorFieldOnly(); + } } diff --git a/code/Model/Submission/SubmittedFileField.php b/code/Model/Submission/SubmittedFileField.php index 8b3c73f4..79f6d611 100755 --- a/code/Model/Submission/SubmittedFileField.php +++ b/code/Model/Submission/SubmittedFileField.php @@ -5,6 +5,7 @@ use SilverStripe\Assets\File; use SilverStripe\Control\Director; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\ManyManyList; use SilverStripe\Versioned\Versioned; use SilverStripe\Security\Member; use SilverStripe\Security\Security; @@ -16,6 +17,7 @@ * @package userforms * @property int $UploadedFileID * @method File UploadedFile() + * @method ManyManyList UploadedFiles() */ class SubmittedFileField extends SubmittedFormField { @@ -23,61 +25,81 @@ class SubmittedFileField extends SubmittedFormField 'UploadedFile' => File::class ]; + private static $many_many = [ + 'UploadedFiles' => File::class + ]; + private static $table_name = 'SubmittedFileField'; private static $owns = [ - 'UploadedFile' + 'UploadedFile', + 'UploadedFiles', ]; private static $cascade_deletes = [ - 'UploadedFile' + 'UploadedFile', + 'UploadedFiles' ]; + /** + * Cache of the uploaded files + * + * @var array|null + */ + private $uploadedFilesCache = []; + /** * Return the value of this field for inclusion into things such as * reports. * - * @return string + * @return string|bool */ public function getFormattedValue() { - $name = $this->getFileName(); - $link = $this->getLink(false); - if ($link) { - $title = _t(__CLASS__ . '.DOWNLOADFILE', 'Download File'); - $file = $this->getUploadedFileFromDraft(); - if (!$file->canView()) { - if (Security::getCurrentUser()) { - // Logged in CMS user without permissions to view file in the CMS - $default = 'You don\'t have the right permissions to download this file'; - $message = _t(__CLASS__ . '.INSUFFICIENTRIGHTS', $default); - return DBField::create_field('HTMLText', sprintf( - ' %s - %s', - htmlspecialchars($name, ENT_QUOTES), - htmlspecialchars($message, ENT_QUOTES) - )); + $title = _t(__CLASS__ . '.DOWNLOADFILE', 'Download File'); + $values = []; + $links = $this->getLinks(false); + if ($links) { + foreach ($this->getUploadedFilesFromDraft() ?: [] as $file) { + if (!$link = $links[$file->ID] ?? null) { + continue; + } + + $name = $file->Name; + + if (!$file->canView()) { + if (Security::getCurrentUser()) { + // Logged in CMS user without permissions to view file in the CMS + $default = 'You don\'t have the right permissions to download this file'; + $message = _t(__CLASS__ . '.INSUFFICIENTRIGHTS', $default); + $values[] = sprintf( + ' %s - %s', + htmlspecialchars($name, ENT_QUOTES), + htmlspecialchars($message, ENT_QUOTES) + ); + } else { + // Userforms submission filled in by non-logged in user being emailed to recipient + $message = _t(__CLASS__ . '.YOUMUSTBELOGGEDIN', 'You must be logged in to view this file'); + $values[] = sprintf( + '%s - %s - %s', + htmlspecialchars($name, ENT_QUOTES), + htmlspecialchars($link, ENT_QUOTES), + htmlspecialchars($title, ENT_QUOTES), + htmlspecialchars($message, ENT_QUOTES) + ); + } } else { - // Userforms submission filled in by non-logged in user being emailed to recipient - $message = _t(__CLASS__ . '.YOUMUSTBELOGGEDIN', 'You must be logged in to view this file'); - return DBField::create_field('HTMLText', sprintf( - '%s - %s - %s', + // Logged in CMS user with permissions to view file in the CMS + $values[] = sprintf( + '%s - %s', htmlspecialchars($name, ENT_QUOTES), htmlspecialchars($link, ENT_QUOTES), - htmlspecialchars($title, ENT_QUOTES), - htmlspecialchars($message, ENT_QUOTES) - )); + htmlspecialchars($title, ENT_QUOTES) + ); } - } else { - // Logged in CMS user with permissions to view file in the CMS - return DBField::create_field('HTMLText', sprintf( - '%s - %s', - htmlspecialchars($name, ENT_QUOTES), - htmlspecialchars($link, ENT_QUOTES), - htmlspecialchars($title, ENT_QUOTES) - )); } } - return false; + return $values ? DBField::create_field('HTMLText', implode('
', $values)) : false; } /** @@ -87,53 +109,67 @@ public function getFormattedValue() */ public function getExportValue() { - return ($link = $this->getLink()) ? $link : ''; + return ($links = $this->getLinks()) ? implode("\r", $links) : ''; } /** * Return the link for the file attached to this submitted form field. * - * @return string + * @return array|null */ - public function getLink($grant = true) + public function getLinks($grant = true) { - if ($file = $this->getUploadedFileFromDraft()) { - if ($file->exists()) { - $url = $file->getURL($grant); - if ($url) { - return Director::absoluteURL($url); + if ($files = $this->getUploadedFilesFromDraft()) { + return array_reduce($files, function ($links, $file) use ($grant) { + if ($file->exists()) { + $url = $file->getURL($grant); + if ($url) { + $links[$file->ID] = Director::absoluteURL($url); + } } - return null; - } + return $links; + }, []); } + return null; } /** * As uploaded files are stored in draft by default, this retrieves the - * uploaded file from draft mode rather than using the current stage. + * uploaded files from draft mode rather than using the current stage. * - * @return File + * @return array|null */ - public function getUploadedFileFromDraft() + public function getUploadedFilesFromDraft() { + if (array_key_exists($this->ID, $this->uploadedFilesCache)) { + return $this->uploadedFilesCache[$this->ID]; + } + $fileId = $this->UploadedFileID; return Versioned::withVersionedMode(function () use ($fileId) { Versioned::set_stage(Versioned::DRAFT); - return File::get()->byID($fileId); + if ($uploadedFiles = $this->UploadedFiles()->map('ID', 'ID')->toArray()) { + $files = File::get()->byIDs($uploadedFiles)->toArray(); + } else { + $files = ($file = File::get()->byID($fileId)) ? [$file] : null; + } + + return $this->uploadedFilesCache[$this->ID] = $files; }); } /** - * Return the name of the file, if present + * Return the names of the files, if present * - * @return string + * @return array|null */ - public function getFileName() + public function getFileNames() { - if ($file = $this->getUploadedFileFromDraft()) { - return $file->Name; + if ($files = $this->getUploadedFilesFromDraft()) { + return array_map(fn ($file) => $file->Name, $files); } + return null; } } diff --git a/tests/php/Model/SubmittedFileFieldTest.php b/tests/php/Model/SubmittedFileFieldTest.php index d88304be..7b703852 100644 --- a/tests/php/Model/SubmittedFileFieldTest.php +++ b/tests/php/Model/SubmittedFileFieldTest.php @@ -47,7 +47,7 @@ protected function tearDown(): void public function testDeletingSubmissionRemovesFile() { - $this->assertStringContainsString('test-SubmittedFileFieldTest', $this->submittedFile->getFileName(), 'Submitted file is linked'); + $this->assertStringContainsString('test-SubmittedFileFieldTest', $this->submittedFile->getFileNames()[0], 'Submitted file is linked'); $this->submittedForm->delete(); $fileId = $this->file->ID; @@ -73,7 +73,7 @@ public function testGetFormattedValue() { // Set an explicit base URL so we get a reliable value for the test Director::config()->set('alternate_base_url', 'http://mysite.com'); - $fileName = $this->submittedFile->getFileName(); + $fileName = $this->submittedFile->getFileNames()[0]; $link = 'http://mysite.com/assets/3c01bdbb26/test-SubmittedFileFieldTest.txt'; $this->file->CanViewType = 'OnlyTheseUsers';