From 288b017355c94c2b614d0e02106604e8eda58d47 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 26 Jan 2024 13:02:49 +0200 Subject: [PATCH] sanitizers --- .../Classes/FieldSanitizers/ArrayFromNull.php | 54 ++++++++++++++++ .../FieldSanitizers/ArrayStringTrim.php | 62 +++++++++++++++++++ .../FieldSanitizers/EmptyStringToNull.php | 58 +++++++++++++++++ .../Classes/FieldSanitizers/StringTrim.php | 60 ++++++++++++++++++ .../Core/FieldSanitize/SanitizeManager.php | 32 +++++++--- .../Espo/Resources/metadata/fields/array.json | 4 ++ .../Resources/metadata/fields/barcode.json | 3 + .../Resources/metadata/fields/checklist.json | 4 ++ .../metadata/fields/colorpicker.json | 3 + .../Espo/Resources/metadata/fields/email.json | 3 + .../Resources/metadata/fields/multiEnum.json | 4 ++ .../Espo/Resources/metadata/fields/phone.json | 3 + .../Espo/Resources/metadata/fields/text.json | 3 + .../Espo/Resources/metadata/fields/url.json | 3 + .../metadata/fields/urlMultiple.json | 4 ++ .../Resources/metadata/fields/varchar.json | 3 + .../Resources/metadata/fields/wysiwyg.json | 3 + schema/metadata/fields.json | 5 ++ .../integration/Espo/Record/SanitizeTest.php | 29 ++++++++- 19 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 application/Espo/Classes/FieldSanitizers/ArrayFromNull.php create mode 100644 application/Espo/Classes/FieldSanitizers/ArrayStringTrim.php create mode 100644 application/Espo/Classes/FieldSanitizers/EmptyStringToNull.php create mode 100644 application/Espo/Classes/FieldSanitizers/StringTrim.php diff --git a/application/Espo/Classes/FieldSanitizers/ArrayFromNull.php b/application/Espo/Classes/FieldSanitizers/ArrayFromNull.php new file mode 100644 index 0000000000..b7753db054 --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/ArrayFromNull.php @@ -0,0 +1,54 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +/** + * @noinspection PhpUnused + */ +class ArrayFromNull implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if ($value !== null) { + return; + } + + $data->set($field, []); + } +} diff --git a/application/Espo/Classes/FieldSanitizers/ArrayStringTrim.php b/application/Espo/Classes/FieldSanitizers/ArrayStringTrim.php new file mode 100644 index 0000000000..9cd0ae7851 --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/ArrayStringTrim.php @@ -0,0 +1,62 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +/** + * @noinspection PhpUnused + */ +class ArrayStringTrim implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if (!is_array($value)) { + return; + } + + foreach ($value as $i => $item) { + if (!is_string($item)) { + continue; + } + + $value[$i] = trim($item); + } + + $data->set($field, $value); + } +} diff --git a/application/Espo/Classes/FieldSanitizers/EmptyStringToNull.php b/application/Espo/Classes/FieldSanitizers/EmptyStringToNull.php new file mode 100644 index 0000000000..c10cc4640a --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/EmptyStringToNull.php @@ -0,0 +1,58 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +/** + * @noinspection PhpUnused + */ +class EmptyStringToNull implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if (!is_string($value)) { + return; + } + + if ($value === '') { + $value = null; + } + + $data->set($field, $value); + } +} diff --git a/application/Espo/Classes/FieldSanitizers/StringTrim.php b/application/Espo/Classes/FieldSanitizers/StringTrim.php new file mode 100644 index 0000000000..f4545a55f5 --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/StringTrim.php @@ -0,0 +1,60 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +/** + * @noinspection PhpUnused + */ +class StringTrim implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if (!is_string($value)) { + return; + } + + $value = trim($value); + + if ($value === '') { + $value = null; + } + + $data->set($field, $value); + } +} diff --git a/application/Espo/Core/FieldSanitize/SanitizeManager.php b/application/Espo/Core/FieldSanitize/SanitizeManager.php index 0a8ee6f068..87326912ef 100644 --- a/application/Espo/Core/FieldSanitize/SanitizeManager.php +++ b/application/Espo/Core/FieldSanitize/SanitizeManager.php @@ -84,6 +84,19 @@ class SanitizeManager * @return Sanitizer[] */ private function getSanitizerList(string $entityType, string $field): array + { + $classNameList = $this->getClassNameList($entityType, $field); + + return array_map( + fn ($className) => $this->injectableFactory->createWith($className, ['entityType' => $entityType]), + $classNameList + ); + } + + /** + * @return class-string[] + */ + private function getClassNameList(string $entityType, string $field): array { $fieldType = $this->fieldUtil->getFieldType($entityType, $field); @@ -91,6 +104,8 @@ class SanitizeManager return []; } + $classNameList = []; + /** @var ?class-string $className */ $className = $this->metadata->get("fields.$fieldType.sanitizerClassName"); @@ -98,17 +113,14 @@ class SanitizeManager $classNameList[] = $className; } - /** @var class-string[] $classNameList */ - $classNameList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerClassNameList") ?? []; + /** @var class-string[] $typeClassNameList */ + $typeClassNameList = $this->metadata->get("fields.$fieldType.sanitizerClassNameList") ?? []; - $classNameList = array_merge( - $className ? [$className] : [], - $classNameList - ); + $classNameList = array_merge($classNameList, $typeClassNameList); - return array_map( - fn ($className) => $this->injectableFactory->createWith($className, ['entityType' => $entityType]), - $classNameList - ); + /** @var class-string[] $fieldClassNameList */ + $fieldClassNameList = $this->metadata->get("entityDefs.$entityType.fields.$field.sanitizerClassNameList") ?? []; + + return array_merge($classNameList, $fieldClassNameList); } } diff --git a/application/Espo/Resources/metadata/fields/array.json b/application/Espo/Resources/metadata/fields/array.json index 3d1d768b9d..9d1db7ada2 100644 --- a/application/Espo/Resources/metadata/fields/array.json +++ b/application/Espo/Resources/metadata/fields/array.json @@ -103,5 +103,9 @@ "add", "remove" ], + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\ArrayFromNull", + "Espo\\Classes\\FieldSanitizers\\ArrayStringTrim" + ], "default": [] } diff --git a/application/Espo/Resources/metadata/fields/barcode.json b/application/Espo/Resources/metadata/fields/barcode.json index 40422dbb0a..558c82654a 100644 --- a/application/Espo/Resources/metadata/fields/barcode.json +++ b/application/Espo/Resources/metadata/fields/barcode.json @@ -55,5 +55,8 @@ "len": 255 }, "validatorClassName": "Espo\\Classes\\FieldValidators\\VarcharType", + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "default": null } diff --git a/application/Espo/Resources/metadata/fields/checklist.json b/application/Espo/Resources/metadata/fields/checklist.json index ff76fb19e8..8ebe4661e2 100644 --- a/application/Espo/Resources/metadata/fields/checklist.json +++ b/application/Espo/Resources/metadata/fields/checklist.json @@ -79,5 +79,9 @@ "translatedOptions": true, "dynamicLogicOptions": true, "personalData": true, + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\ArrayFromNull", + "Espo\\Classes\\FieldSanitizers\\ArrayStringTrim" + ], "default": [] } diff --git a/application/Espo/Resources/metadata/fields/colorpicker.json b/application/Espo/Resources/metadata/fields/colorpicker.json index c5033abac3..02d42d0dcc 100644 --- a/application/Espo/Resources/metadata/fields/colorpicker.json +++ b/application/Espo/Resources/metadata/fields/colorpicker.json @@ -27,5 +27,8 @@ "type": "varchar", "maxLength": 7 }, + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "notCreatable": true } diff --git a/application/Espo/Resources/metadata/fields/email.json b/application/Espo/Resources/metadata/fields/email.json index 5fd24e0450..71acd63a8e 100644 --- a/application/Espo/Resources/metadata/fields/email.json +++ b/application/Espo/Resources/metadata/fields/email.json @@ -58,5 +58,8 @@ "personalData": true, "valueFactoryClassName": "Espo\\Core\\Field\\EmailAddress\\EmailAddressGroupFactory", "attributeExtractorClassName": "Espo\\Core\\Field\\EmailAddress\\EmailAddressGroupAttributeExtractor", + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "default": null } diff --git a/application/Espo/Resources/metadata/fields/multiEnum.json b/application/Espo/Resources/metadata/fields/multiEnum.json index b25fcd5cc5..a6a18e64db 100644 --- a/application/Espo/Resources/metadata/fields/multiEnum.json +++ b/application/Espo/Resources/metadata/fields/multiEnum.json @@ -111,5 +111,9 @@ "add", "remove" ], + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\ArrayFromNull", + "Espo\\Classes\\FieldSanitizers\\ArrayStringTrim" + ], "default": [] } diff --git a/application/Espo/Resources/metadata/fields/phone.json b/application/Espo/Resources/metadata/fields/phone.json index 92c1c5bd2b..90d964882b 100644 --- a/application/Espo/Resources/metadata/fields/phone.json +++ b/application/Espo/Resources/metadata/fields/phone.json @@ -78,5 +78,8 @@ "valueFactoryClassName": "Espo\\Core\\Field\\PhoneNumber\\PhoneNumberGroupFactory", "attributeExtractorClassName": "Espo\\Core\\Field\\PhoneNumber\\PhoneNumberGroupAttributeExtractor", "sanitizerClassName": "Espo\\Classes\\FieldSanitizers\\Phone", + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "default": null } diff --git a/application/Espo/Resources/metadata/fields/text.json b/application/Espo/Resources/metadata/fields/text.json index c16919a3d6..5396832124 100644 --- a/application/Espo/Resources/metadata/fields/text.json +++ b/application/Espo/Resources/metadata/fields/text.json @@ -64,5 +64,8 @@ "textFilter": true, "textFilterForeign": true, "fullTextSearch": true, + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\EmptyStringToNull" + ], "default": null } diff --git a/application/Espo/Resources/metadata/fields/url.json b/application/Espo/Resources/metadata/fields/url.json index 9d4f81df4c..f078b892a4 100644 --- a/application/Espo/Resources/metadata/fields/url.json +++ b/application/Espo/Resources/metadata/fields/url.json @@ -49,6 +49,9 @@ "fieldDefs": { "type": "varchar" }, + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "personalData": true, "default": null } diff --git a/application/Espo/Resources/metadata/fields/urlMultiple.json b/application/Espo/Resources/metadata/fields/urlMultiple.json index 23bcf003f9..2151334d66 100644 --- a/application/Espo/Resources/metadata/fields/urlMultiple.json +++ b/application/Espo/Resources/metadata/fields/urlMultiple.json @@ -55,5 +55,9 @@ "add", "remove" ], + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\ArrayFromNull", + "Espo\\Classes\\FieldSanitizers\\ArrayStringTrim" + ], "default": [] } diff --git a/application/Espo/Resources/metadata/fields/varchar.json b/application/Espo/Resources/metadata/fields/varchar.json index 7cc0f2ea8a..5056470aa0 100644 --- a/application/Espo/Resources/metadata/fields/varchar.json +++ b/application/Espo/Resources/metadata/fields/varchar.json @@ -66,6 +66,9 @@ "mandatoryValidationList": [ "maxLength" ], + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\StringTrim" + ], "filter": true, "personalData": true, "textFilter": true, diff --git a/application/Espo/Resources/metadata/fields/wysiwyg.json b/application/Espo/Resources/metadata/fields/wysiwyg.json index 8e47a1f9bb..d3a6f11c8b 100644 --- a/application/Espo/Resources/metadata/fields/wysiwyg.json +++ b/application/Espo/Resources/metadata/fields/wysiwyg.json @@ -57,5 +57,8 @@ "fullTextSearch": true, "duplicatorClassName": "Espo\\Classes\\FieldDuplicators\\Wysiwyg", "validatorClassName": "Espo\\Classes\\FieldValidators\\TextType", + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\EmptyStringToNull" + ], "default": null } diff --git a/schema/metadata/fields.json b/schema/metadata/fields.json index b4a331035e..75c6d40df5 100644 --- a/schema/metadata/fields.json +++ b/schema/metadata/fields.json @@ -171,6 +171,11 @@ "sanitizerClassName": { "type": "string", "description": "A sanitizer. Should implement Espo\\Core\\FieldSanitize\\Sanitizer." + }, + "sanitizerClassNameList": { + "type": "array", + "items": {"type": "string"}, + "description": "Sanitizers. Classes should implement Espo\\Core\\FieldSanitize\\Sanitizer. As of v8.2." } } } diff --git a/tests/integration/Espo/Record/SanitizeTest.php b/tests/integration/Espo/Record/SanitizeTest.php index c3f59fcc4d..153b1878aa 100644 --- a/tests/integration/Espo/Record/SanitizeTest.php +++ b/tests/integration/Espo/Record/SanitizeTest.php @@ -32,12 +32,23 @@ namespace tests\integration\Espo\Record; use Espo\Core\Record\CreateParams; use Espo\Core\Record\ServiceContainer; use Espo\Modules\Crm\Entities\Account; +use Espo\Tools\FieldManager\FieldManager; use tests\integration\Core\BaseTestCase; class SanitizeTest extends BaseTestCase { public function testSanitize(): void { + /** @noinspection PhpUnhandledExceptionInspection */ + $this->getInjectableFactory() + ->create(FieldManager::class) + ->create(Account::ENTITY_TYPE, 'array', ['type' => 'array']); + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->getDataManager()->rebuild(); + + $this->reCreateApplication(); + $service = $this->getContainer() ->getByClass(ServiceContainer::class) ->getByClass(Account::class); @@ -45,14 +56,25 @@ class SanitizeTest extends BaseTestCase /** @noinspection PhpUnhandledExceptionInspection */ /** @var Account $account */ $account = $service->create((object) [ - 'name' => 'Test 1', + 'name' => ' Test 1 ', + 'sicCode' => ' ', 'phoneNumber' => '+380 9044 433 11', + 'description' => '', + 'array' => [ + ' test ', + 'hello', + ], ], CreateParams::create()); $numbers = $account->getPhoneNumberGroup()->getNumberList(); $this->assertCount(1, $numbers); $this->assertEquals('+380904443311', $numbers[0]); + $this->assertEquals('Test 1', $account->getName()); + $this->assertEquals(null, $account->get('sicCode')); + $this->assertEquals(null, $account->get('description')); + $this->assertEquals(['test', 'hello'], $account->get('array')); + /** @noinspection PhpUnhandledExceptionInspection */ /** @var Account $account */ $account = $service->create((object) [ @@ -65,8 +87,13 @@ class SanitizeTest extends BaseTestCase 'phoneNumber' => '+38 09 044 433 33', ], ], + 'description' => 'Test', + 'array' => null, ], CreateParams::create()); + $this->assertEquals('Test', $account->get('description')); + $this->assertEquals([], $account->get('array')); + $numbers = $account->getPhoneNumberGroup()->getNumberList(); $this->assertCount(2, $numbers);