From 0ff72734dd54c291a28f78b611ab1061f5b17a81 Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Sat, 31 Oct 2020 21:15:00 +1300 Subject: [PATCH] NEW Maximum email attachment size can be configured Previously hard coded size of 1MB meant any file larger was excluded from recipient emails - often confusing for CMS admins configuring an advanced use case for a userform, expecting to recieve files to begin a business process external to the website. The configuration has been made to support PHP 'shorthand byte values' https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes in the configuration option. This give flexibility around oddly specific limits, but can also be used to set as 0, disabling attachments Tests are updated to set up a known state before testing, as to be able to accurately assert the results. One should test the class they intend, not an Injector service of some unknown element. --- code/Control/UserDefinedFormController.php | 52 +++++++++++- .../Control/UserDefinedFormControllerTest.php | 79 ++++++++++++++++++- .../fixtures/SizeStringTestableController.php | 14 ++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/php/Control/fixtures/SizeStringTestableController.php diff --git a/code/Control/UserDefinedFormController.php b/code/Control/UserDefinedFormController.php index 4a1d8a8..8d838dc 100644 --- a/code/Control/UserDefinedFormController.php +++ b/code/Control/UserDefinedFormController.php @@ -56,6 +56,16 @@ class UserDefinedFormController extends PageController private static string $file_upload_stage = Versioned::DRAFT; + /** + * Size that an uploaded file must not excede for it to be attached to an email + * Follows PHP "shorthand bytes" definition rules. + * @see self::parseByteSizeString() + * + * @var int + * @config + */ + private static $maximum_email_attachment_size = '1M'; + protected function init() { parent::init(); @@ -215,6 +225,42 @@ JS } } + /** + * Returns the maximum size uploaded files can be before they're excluded from CMS configured recipient emails + * + * @return int size in megabytes + */ + public function getMaximumAllowedEmailAttachmentSize() + { + return $this->parseByteSizeString($this->config()->get('maximum_email_attachment_size')); + } + + /** + * Convert file sizes with a single character for unit size to true byte count. + * Just as with php.ini and e.g. 128M -> 1024 * 1024 * 128 bytes. + * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + * + * @param string $byteSize + * @return int bytes + */ + protected function parseByteSizeString($byteSize) + { + // kilo, mega, giga + $validUnits = 'kmg'; + $valid = preg_match("/^(?\d+)((?[$validUnits])b?)?$/i", $byteSize, $matches); + if (!$valid) { + throw new \InvalidArgumentException( + "Expected a positive integer followed optionally by K, M, or G. Found '$byteSize' instead" + ); + } + $power = 0; + // prepend b for bytes to $validUnits to give correct mapping of ordinal position to exponent + if (isset($matches['unit'])) { + $power = stripos("b$validUnits", $matches['unit']); + } + return intval($matches['number']) * pow(1024, $power); + } + /** * Process the form that is submitted through the site * @@ -268,7 +314,7 @@ JS if (!$field->getFolderExists()) { $field->createProtectedFolder(); } - + $file = Versioned::withVersionedMode(function () use ($field, $form) { $stage = Injector::inst()->get(self::class)->config()->get('file_upload_stage'); Versioned::set_stage($stage); @@ -308,8 +354,8 @@ JS // write file to form field $submittedField->UploadedFileID = $file->ID; - // attach a file only if lower than 1MB - if ($file->getAbsoluteSize() < 1024 * 1024 * 1) { + // attach a file to recipient email only if lower than configured size + if ($file->getAbsoluteSize() <= $this->getMaximumAllowedEmailAttachmentSize()) { $attachments[] = $file; } } diff --git a/tests/php/Control/UserDefinedFormControllerTest.php b/tests/php/Control/UserDefinedFormControllerTest.php index 4e1e716..8ccd362 100644 --- a/tests/php/Control/UserDefinedFormControllerTest.php +++ b/tests/php/Control/UserDefinedFormControllerTest.php @@ -24,6 +24,7 @@ use SilverStripe\UserForms\Model\EditableFormField\EditableTextField; use SilverStripe\UserForms\Model\Recipient\EmailRecipient; use SilverStripe\UserForms\Model\Submission\SubmittedFormField; use SilverStripe\UserForms\Model\UserDefinedForm; +use SilverStripe\UserForms\Tests\Control\fixtures\SizeStringTestableController; use SilverStripe\Versioned\Versioned; use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; @@ -47,7 +48,9 @@ class UserDefinedFormControllerTest extends FunctionalTest // Set backend and base url TestAssetStore::activate('AssetStoreTest'); - Config::modify()->merge(SSViewer::class, 'themes', ['simple', '$default']); + $config = Config::modify(); + $config->set(UserDefinedFormController::class, 'maximum_email_attachment_size', "1M"); + $config->merge(SSViewer::class, 'themes', ['simple', '$default']); } protected function tearDown(): void @@ -273,7 +276,8 @@ class UserDefinedFormControllerTest extends FunctionalTest $actions = $controller->Form()->getFormActions(); $expected = new FieldList(new FormAction('process', 'Custom Button')); - $expected->push(FormAction::create('clearForm', 'Clear')->setAttribute('type', 'reset')); + $clearAction = new FormAction('clearForm', 'Clear'); + $expected->push($clearAction->setAttribute('type', 'reset')); $expected->setForm($controller->Form()); $this->assertEquals($actions, $expected); @@ -404,7 +408,7 @@ class UserDefinedFormControllerTest extends FunctionalTest Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false); $userForm = $this->setupFormFrontend('upload-form'); - $controller = UserDefinedFormController::create($userForm); + $controller = new UserDefinedFormController($userForm); $field = $this->objFromFixture(EditableFileField::class, 'file-field-1'); $path = realpath(__DIR__ . '/fixtures/testfile.jpg'); @@ -440,7 +444,7 @@ class UserDefinedFormControllerTest extends FunctionalTest Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false); $userForm = $this->setupFormFrontend('upload-form'); - $controller = UserDefinedFormController::create($userForm); + $controller = new UserDefinedFormController($userForm); $field = $this->objFromFixture(EditableFileField::class, 'file-field-1'); $path = realpath(__DIR__ . '/fixtures/testfile.jpg'); @@ -511,4 +515,71 @@ class UserDefinedFormControllerTest extends FunctionalTest $this->assertEquals(1, $fileDraftCount); $this->assertEquals(0, $fileLiveCount); } + + public function testEmailAttachmentMaximumSizeCanBeConfigured() + { + $udfController = new UserDefinedFormController(); + $config = Config::modify(); + $config->set(UserDefinedFormController::class, 'maximum_email_attachment_size', '1M'); + $this->assertSame(1 * 1024 * 1024, $udfController->getMaximumAllowedEmailAttachmentSize()); + $config->set(UserDefinedFormController::class, 'maximum_email_attachment_size', '5M'); + $this->assertSame(5 * 1024 * 1024, $udfController->getMaximumAllowedEmailAttachmentSize()); + } + + public function getParseByteSizeStringTestValues() + { + return [ + ['9846', 9846], + ['1048576', 1048576], + ['1k', 1024], + ['1K', 1024], + ['4k', 4096], + ['4K', 4096], + ['1kb', 1024], + ['1KB', 1024], + ['4kB', 4096], + ['4Kb', 4096], + ['1m', 1048576], + ['1M', 1048576], + ['4mb', 4194304], + ['4MB', 4194304], + ['25mB', 26214400], + ['25Mb', 26214400], + ['1g', 1073741824], + ['2GB', 2147483648], + ]; + } + + /** + * @dataProvider getParseByteSizeStringTestValues + */ + public function testParseByteSizeString($input, $expectedOutput) + { + $controller = new SizeStringTestableController(); // extends UserDefinedFormController + $this->assertSame($expectedOutput, $controller->convertSizeStringToBytes($input)); + } + + public function getParseByteSizeStringTestBadValues() + { + return [ + ['1234b'], + ['9846B'], + ['1kilobyte'], + ['1 K'], + ['Four kilobytes'], + ['4Mbs'], + ['12Gigs'], + ]; + } + + /** + * @dataProvider getParseByteSizeStringTestBadValues + * @expectedException \InvalidArgumentException + */ + public function testParseByteSizeStringBadValuesThrowException($input) + { + $this->expectException('\InvalidArgumentException'); + $controller = new SizeStringTestableController(); // extends UserDefinedFormController + $controller->convertSizeStringToBytes($input); + } } diff --git a/tests/php/Control/fixtures/SizeStringTestableController.php b/tests/php/Control/fixtures/SizeStringTestableController.php new file mode 100644 index 0000000..9d6a36f --- /dev/null +++ b/tests/php/Control/fixtures/SizeStringTestableController.php @@ -0,0 +1,14 @@ +parseByteSizeString($sizeString); + } +}