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.
This commit is contained in:
Dylan Wagstaff 2020-10-31 21:15:00 +13:00 committed by Dylan Wagstaff
parent 25e64441b8
commit 0ff72734dd
3 changed files with 138 additions and 7 deletions

View File

@ -56,6 +56,16 @@ class UserDefinedFormController extends PageController
private static string $file_upload_stage = Versioned::DRAFT; 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() protected function init()
{ {
parent::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("/^(?<number>\d+)((?<unit>[$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 * Process the form that is submitted through the site
* *
@ -308,8 +354,8 @@ JS
// write file to form field // write file to form field
$submittedField->UploadedFileID = $file->ID; $submittedField->UploadedFileID = $file->ID;
// attach a file only if lower than 1MB // attach a file to recipient email only if lower than configured size
if ($file->getAbsoluteSize() < 1024 * 1024 * 1) { if ($file->getAbsoluteSize() <= $this->getMaximumAllowedEmailAttachmentSize()) {
$attachments[] = $file; $attachments[] = $file;
} }
} }

View File

@ -24,6 +24,7 @@ use SilverStripe\UserForms\Model\EditableFormField\EditableTextField;
use SilverStripe\UserForms\Model\Recipient\EmailRecipient; use SilverStripe\UserForms\Model\Recipient\EmailRecipient;
use SilverStripe\UserForms\Model\Submission\SubmittedFormField; use SilverStripe\UserForms\Model\Submission\SubmittedFormField;
use SilverStripe\UserForms\Model\UserDefinedForm; use SilverStripe\UserForms\Model\UserDefinedForm;
use SilverStripe\UserForms\Tests\Control\fixtures\SizeStringTestableController;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
use SilverStripe\View\ArrayData; use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer; use SilverStripe\View\SSViewer;
@ -47,7 +48,9 @@ class UserDefinedFormControllerTest extends FunctionalTest
// Set backend and base url // Set backend and base url
TestAssetStore::activate('AssetStoreTest'); 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 protected function tearDown(): void
@ -273,7 +276,8 @@ class UserDefinedFormControllerTest extends FunctionalTest
$actions = $controller->Form()->getFormActions(); $actions = $controller->Form()->getFormActions();
$expected = new FieldList(new FormAction('process', 'Custom Button')); $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()); $expected->setForm($controller->Form());
$this->assertEquals($actions, $expected); $this->assertEquals($actions, $expected);
@ -404,7 +408,7 @@ class UserDefinedFormControllerTest extends FunctionalTest
Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false); Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false);
$userForm = $this->setupFormFrontend('upload-form'); $userForm = $this->setupFormFrontend('upload-form');
$controller = UserDefinedFormController::create($userForm); $controller = new UserDefinedFormController($userForm);
$field = $this->objFromFixture(EditableFileField::class, 'file-field-1'); $field = $this->objFromFixture(EditableFileField::class, 'file-field-1');
$path = realpath(__DIR__ . '/fixtures/testfile.jpg'); $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); Config::modify()->set(Upload_Validator::class, 'use_is_uploaded_file', false);
$userForm = $this->setupFormFrontend('upload-form'); $userForm = $this->setupFormFrontend('upload-form');
$controller = UserDefinedFormController::create($userForm); $controller = new UserDefinedFormController($userForm);
$field = $this->objFromFixture(EditableFileField::class, 'file-field-1'); $field = $this->objFromFixture(EditableFileField::class, 'file-field-1');
$path = realpath(__DIR__ . '/fixtures/testfile.jpg'); $path = realpath(__DIR__ . '/fixtures/testfile.jpg');
@ -511,4 +515,71 @@ class UserDefinedFormControllerTest extends FunctionalTest
$this->assertEquals(1, $fileDraftCount); $this->assertEquals(1, $fileDraftCount);
$this->assertEquals(0, $fileLiveCount); $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);
}
} }

View File

@ -0,0 +1,14 @@
<?php
namespace SilverStripe\UserForms\Tests\Control\fixtures;
use SilverStripe\Dev\TestOnly;
use SilverStripe\UserForms\Control\UserDefinedFormController;
class SizeStringTestableController extends UserDefinedFormController implements TestOnly
{
public function convertSizeStringToBytes($sizeString)
{
return $this->parseByteSizeString($sizeString);
}
}