diff --git a/typo3/sysext/backend/Classes/Form/Element/UuidElement.php b/typo3/sysext/backend/Classes/Form/Element/UuidElement.php new file mode 100644 index 0000000000000000000000000000000000000000..23e29a80f09e8867ea52d262df19d947b0bdb5e5 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/Element/UuidElement.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Form\Element; + +use Symfony\Component\Uid\Uuid; +use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\MathUtility; +use TYPO3\CMS\Core\Utility\StringUtility; + +/** + * Render a readonly input field, which is filled with a UUID + */ +class UuidElement extends AbstractFormElement +{ + /** + * Default field information enabled for this element. + * + * @var array + */ + protected $defaultFieldInformation = [ + 'tcaDescription' => [ + 'renderType' => 'tcaDescription', + ], + ]; + + public function render(): array + { + $resultArray = $this->initializeResultArray(); + $parameterArray = $this->data['parameterArray']; + $itemValue = htmlspecialchars((string)$parameterArray['itemFormElValue'], ENT_QUOTES); + $config = $parameterArray['fieldConf']['config']; + $itemName = $parameterArray['itemFormElName']; + $fieldId = StringUtility::getUniqueId('formengine-uuid-'); + + if (!isset($config['required'])) { + $config['required'] = true; + } + + if ($config['required'] && !Uuid::isValid($itemValue)) { + // Note: This can only happen in case the TcaUuid data provider is not executed or a custom + // data provider has changed the value afterwards. Since this can only happen in user code, + // we throw an exception to inform the administrator about this misconfiguration. + throw new \RuntimeException( + 'Field "' . $this->data['fieldName'] . '" in table "' . $this->data['tableName'] . '" of type "uuid" defines the field to be required but does not contain a valid uuid. Make sure to properly generate a valid uuid value.', + 1678895476 + ); + } + + $width = $this->formMaxWidth( + MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth) + ); + + $attributes = [ + 'id' => $fieldId, + 'name' => $itemName, + 'type' => 'text', + 'readonly' => 'readonly', + 'class' => 'form-control disabled', + 'data-formengine-input-name' => $itemName, + 'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config), + ]; + + $uuidElement = ' + <input value="' . $itemValue . '" + ' . GeneralUtility::implodeAttributes($attributes, true) . ' + />'; + + $fieldInformationResult = $this->renderFieldInformation(); + $fieldInformationHtml = $fieldInformationResult['html']; + $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); + + if (($config['enableCopyToClipboard'] ?? true) !== false) { + $uuidElement = ' + <div class="input-group"> + ' . $uuidElement . ' + <typo3-copy-to-clipboard text="' . $itemValue . '"> + <button type="button" class="btn btn-default" title="' . htmlspecialchars(sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_copytoclipboard.xlf:copyToClipboard.title'), 'UUID')) . '"> + ' . $this->iconFactory->getIcon('actions-clipboard', Icon::SIZE_SMALL) . ' + </button> + </typo3-copy-to-clipboard> + </div>'; + + $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/copy-to-clipboard.js'); + } + + $html = []; + $html[] = '<div class="formengine-field-item t3js-formengine-field-item">'; + $html[] = $fieldInformationHtml; + $html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">'; + $html[] = '<div class="form-wizards-wrap">'; + $html[] = '<div class="form-wizards-element">'; + $html[] = $uuidElement; + $html[] = '</div>'; + $html[] = '</div>'; + $html[] = '</div>'; + $html[] = '</div>'; + + $resultArray['html'] = implode(LF, $html); + + return $resultArray; + } +} diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php index 273964dff8960f7b7c31c4b7ed86984fbdc2fb12..754a9b001802cf98d19fbc6059719fdceb95c43e 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php @@ -177,6 +177,7 @@ class TcaRecordTitle implements FormDataProviderInterface break; case 'input': case 'number': + case 'uuid': $recordTitle = $rawValue ?? ''; break; case 'text': diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaUuid.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaUuid.php new file mode 100644 index 0000000000000000000000000000000000000000..0cbb8b504880b21c006981166e84506b3e22c795 --- /dev/null +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaUuid.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Form\FormDataProvider; + +use Symfony\Component\Uid\Uuid; +use TYPO3\CMS\Backend\Form\FormDataProviderInterface; + +/** + * Generates and sets field value for type=uuid + */ +class TcaUuid implements FormDataProviderInterface +{ + public function addData(array $result): array + { + foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) { + if (($fieldConfig['config']['type'] ?? '') !== 'uuid') { + continue; + } + + // Skip if field is already filled with a valid uuid + if (Uuid::isValid((string)($result['databaseRow'][$fieldName] ?? ''))) { + continue; + } + + $result['databaseRow'][$fieldName] = (string)match ((int)($fieldConfig['config']['version'] ?? 0)) { + 6 => Uuid::v6(), + 7 => Uuid::v7(), + default => Uuid::v4() + }; + } + + return $result; + } +} diff --git a/typo3/sysext/backend/Classes/Form/NodeFactory.php b/typo3/sysext/backend/Classes/Form/NodeFactory.php index 069f8f5949a16827be27859ca88cf8fb7ce4373b..eb9364e3c8a7b6609d330aa17f76c5448907c5cb 100644 --- a/typo3/sysext/backend/Classes/Form/NodeFactory.php +++ b/typo3/sysext/backend/Classes/Form/NodeFactory.php @@ -108,6 +108,7 @@ class NodeFactory 'passthrough' => Element\PassThroughElement::class, 'belayoutwizard' => Element\BackendLayoutWizardElement::class, 'json' => Element\JsonElement::class, + 'uuid' => Element\UuidElement::class, // Default classes to enrich single elements 'fieldControl' => NodeExpansion\FieldControl::class, diff --git a/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php b/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php index fc8b3715fd194517faa5134fc5e7e7695a44aab4..06500e94e373e5869ecae567bd27278a07367038 100644 --- a/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php +++ b/typo3/sysext/backend/Classes/Form/Utility/FormEngineUtility.php @@ -51,6 +51,7 @@ class FormEngineUtility 'password' => ['size', 'readOnly'], 'datetime' => ['size', 'readOnly'], 'color' => ['size', 'readOnly'], + 'uuid' => ['size', 'enableCopyToClipboard'], 'text' => ['cols', 'rows', 'wrap', 'max', 'readOnly'], 'json' => ['cols', 'rows', 'readOnly'], 'check' => ['cols', 'readOnly'], diff --git a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php index 245a4be3bbc720b563655f402c18ed6f8540ec23..ce1be6618e31386ac66359a6aa8b507db83fce9c 100644 --- a/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php +++ b/typo3/sysext/backend/Classes/RecordList/DatabaseRecordList.php @@ -2389,15 +2389,7 @@ class DatabaseRecordList $expressionBuilder->eq($tablePidField, (int)$currentPid) ); } - } elseif ($fieldType === 'input' - || $fieldType === 'text' - || $fieldType === 'json' - || $fieldType === 'flex' - || $fieldType === 'email' - || $fieldType === 'link' - || $fieldType === 'slug' - || $fieldType === 'color' - ) { + } elseif ($this->isTextFieldType($fieldType)) { $constraints[] = $expressionBuilder->like( $fieldName, $queryBuilder->quote('%' . (int)$this->searchString . '%') @@ -2434,18 +2426,8 @@ class DatabaseRecordList ); } } - if ($fieldType === 'input' - || $fieldType === 'text' - || $fieldType === 'json' - || $fieldType === 'flex' - || $fieldType === 'email' - || $fieldType === 'link' - || $fieldType === 'slug' - || $fieldType === 'color' - ) { - if ($searchConstraint->count() !== 0) { - $constraints[] = $searchConstraint; - } + if ($this->isTextFieldType($fieldType) && $searchConstraint->count() !== 0) { + $constraints[] = $searchConstraint; } } } @@ -3156,7 +3138,7 @@ class DatabaseRecordList } /** - * Add a divider to the secondary cell gorup, if not already present + * Add a divider to the secondary cell group, if not already present */ protected function addDividerToCellGroup(array &$cells): void { @@ -3164,4 +3146,21 @@ class DatabaseRecordList $this->addActionToCellGroup($cells, '<hr class="dropdown-divider">', 'divider'); } } + + protected function isTextFieldType(string $fieldType): bool + { + $textFieldTypes = [ + 'input', + 'text', + 'json', + 'flex', + 'email', + 'link', + 'slug', + 'color', + 'uuid', + ]; + + return in_array($fieldType, $textFieldTypes, true); + } } diff --git a/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php b/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php index 0b25202935e5f179443fd862a585ae94159c7b75..c5db1b805198f0595529cfeaedf1d1d456939aa8 100644 --- a/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php +++ b/typo3/sysext/backend/Classes/Search/LiveSearch/DatabaseRecordProvider.php @@ -346,6 +346,7 @@ final class DatabaseRecordProvider implements SearchProviderInterface 'link', 'color', 'slug', + 'uuid', ]; return in_array($fieldType, $searchableFieldTypes, true); diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang_copytoclipboard.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang_copytoclipboard.xlf index b26372b9ae12c5b99cb5d96d87ac0a846cb5cd55..4c9609bdd1c32b5ecfbf716e6a4cd870b456657f 100644 --- a/typo3/sysext/backend/Resources/Private/Language/locallang_copytoclipboard.xlf +++ b/typo3/sysext/backend/Resources/Private/Language/locallang_copytoclipboard.xlf @@ -3,6 +3,9 @@ <file source-language="en" datatype="plaintext" original="EXT:backend/Resources/Private/Language/locallang_copytoclipboard.xlf" date="2021-06-01T12:43:12Z" product-name="backend"> <header/> <body> + <trans-unit id="copyToClipboard.title" resname="copyToClipboard.title"> + <source>Copy %s to clipboard</source> + </trans-unit> <trans-unit id="copyToClipboard.success" resname="copyToClipboard.success"> <source>Copied to clipboard</source> </trans-unit> diff --git a/typo3/sysext/backend/Tests/Unit/Form/Element/UuidElementTest.php b/typo3/sysext/backend/Tests/Unit/Form/Element/UuidElementTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f8b7379388e2b7e7915ab1e8ff1faf99e5d5299c --- /dev/null +++ b/typo3/sysext/backend/Tests/Unit/Form/Element/UuidElementTest.php @@ -0,0 +1,164 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Unit\Form\Element; + +use TYPO3\CMS\Backend\Form\Element\UuidElement; +use TYPO3\CMS\Backend\Form\NodeExpansion\FieldInformation; +use TYPO3\CMS\Backend\Form\NodeFactory; +use TYPO3\CMS\Core\Imaging\IconFactory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class UuidElementTest extends UnitTestCase +{ + protected bool $resetSingletonInstances = true; + + protected function setUp(): void + { + parent::setUp(); + $GLOBALS['LANG'] = $this->createMock(LanguageService::class); + } + + /** + * @test + */ + public function renderThrowsExceptionOnEmptyElementValue(): void + { + $data = [ + 'tableName' => 'aTable', + 'fieldName' => 'identifier', + 'parameterArray' => [ + 'itemFormElName' => 'identifier', + 'itemFormElValue' => '', + 'fieldConf' => [ + 'config' => [ + 'type' => 'uuid', + 'required' => true, + ], + ], + ], + ]; + + GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1678895476); + + (new UuidElement($this->createMock(NodeFactory::class), $data))->render(); + } + + /** + * @test + */ + public function renderThrowsExceptionOnInvalidUuid(): void + { + $data = [ + 'tableName' => 'aTable', + 'fieldName' => 'identifier', + 'parameterArray' => [ + 'itemFormElName' => 'identifier', + 'itemFormElValue' => '_-invalid-_', + 'fieldConf' => [ + 'config' => [ + 'type' => 'uuid', + 'required' => true, + ], + ], + ], + ]; + + GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class)); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionCode(1678895476); + + (new UuidElement($this->createMock(NodeFactory::class), $data))->render(); + } + + /** + * @test + */ + public function renderReturnsInputElementWithUuidAndCopyToClipboardButton(): void + { + $uuid = 'b3190536-1431-453e-afbb-25b8c5022513'; + $data = [ + 'tableName' => 'aTable', + 'fieldName' => 'identifier', + 'parameterArray' => [ + 'itemFormElName' => 'identifier', + 'itemFormElValue' => $uuid, + 'fieldConf' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ]; + + GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class)); + + $nodeFactoryMock = $this->createMock(NodeFactory::class); + $fieldInformationMock = $this->createMock(FieldInformation::class); + $fieldInformationMock->method('render')->willReturn(['html' => '']); + $nodeFactoryMock->method('create')->with(self::anything())->willReturn($fieldInformationMock); + + $subject = new UuidElement($nodeFactoryMock, $data); + $result = $subject->render(); + + self::assertEquals('@typo3/backend/copy-to-clipboard.js', $result['javaScriptModules'][0]->getName()); + self::assertMatchesRegularExpression('/<typo3-copy-to-clipboard.*text="' . $uuid . '"/s', $result['html']); + self::assertMatchesRegularExpression('/<input.*value="' . $uuid . '".*id="formengine-uuid-/s', $result['html']); + } + + /** + * @test + */ + public function renderReturnsInputElementWithUuidAndWithoutCopyToClipboardButton(): void + { + $uuid = 'b3190536-1431-453e-afbb-25b8c5022513'; + $data = [ + 'tableName' => 'aTable', + 'fieldName' => 'identifier', + 'parameterArray' => [ + 'itemFormElName' => 'identifier', + 'itemFormElValue' => $uuid, + 'fieldConf' => [ + 'config' => [ + 'type' => 'uuid', + 'enableCopyToClipboard' => false, + ], + ], + ], + ]; + + GeneralUtility::addInstance(IconFactory::class, $this->createMock(IconFactory::class)); + + $nodeFactoryMock = $this->createMock(NodeFactory::class); + $fieldInformationMock = $this->createMock(FieldInformation::class); + $fieldInformationMock->method('render')->willReturn(['html' => '']); + $nodeFactoryMock->method('create')->with(self::anything())->willReturn($fieldInformationMock); + + $subject = new UuidElement($nodeFactoryMock, $data); + $result = $subject->render(); + + self::assertEmpty($result['javaScriptModules']); + self::assertDoesNotMatchRegularExpression('/<typo3-copy-to-clipboard.*text="' . $uuid . '"/s', $result['html']); + self::assertMatchesRegularExpression('/<input.*value="' . $uuid . '".*id="formengine-uuid-/s', $result['html']); + } +} diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaUuidTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaUuidTest.php new file mode 100644 index 0000000000000000000000000000000000000000..303a93beea9e2e7f938ac4e78d78136091dab5ea --- /dev/null +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaUuidTest.php @@ -0,0 +1,226 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataProvider; + +use Symfony\Component\Uid\Uuid; +use TYPO3\CMS\Backend\Form\FormDataProvider\TcaUuid; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +class TcaUuidTest extends UnitTestCase +{ + public function resultArrayDataProvider(): \Generator + { + yield 'Only handles TCA type "uuid" records' => [ + [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'input', + ], + ], + ], + ], + ], + '', + ]; + yield 'Does not handle records with valid uuid value' => [ + [ + 'command' => 'edit', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => 'b3190536-1431-453e-afbb-25b8c5022513', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ], + 'b3190536-1431-453e-afbb-25b8c5022513', + ]; + yield 'Does handle records with invalid uuid value' => [ + [ + 'command' => 'edit', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '_-invalid-_', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ], + 'b3190536-1431-453e-afbb-25b8c5022513', + ]; + } + + /** + * @test + */ + public function addDataDoesOnlyHandleTypeUuid(): void + { + $input = [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'input', + ], + ], + ], + ], + ]; + + self::assertSame('', (new TcaUuid())->addData($input)['databaseRow']['aField']); + } + + /** + * @test + */ + public function addDataDoesNotHandleFieldsWithValidUuidValue(): void + { + $input = [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => 'b3190536-1431-453e-afbb-25b8c5022513', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ]; + + self::assertSame('b3190536-1431-453e-afbb-25b8c5022513', (new TcaUuid())->addData($input)['databaseRow']['aField']); + } + + /** + * @test + */ + public function addDataCreatesValidUuidValueForInvalidUuid(): void + { + $input = [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '_-invalid-_', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ]; + + self::assertFalse(Uuid::isValid($input['databaseRow']['aField'])); + self::assertTrue(Uuid::isValid((new TcaUuid())->addData($input)['databaseRow']['aField'])); + } + + /** + * @test + */ + public function addDataCreatesValidUuidValueForEmptyField(): void + { + $input = [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ]; + + self::assertFalse(Uuid::isValid($input['databaseRow']['aField'])); + self::assertTrue(Uuid::isValid((new TcaUuid())->addData($input)['databaseRow']['aField'])); + } + + /** + * @test + */ + public function addDataCreatesValidUuidValueWithDefinedVersion(): void + { + $input = [ + 'command' => 'new', + 'tableName' => 'aTable', + 'databaseRow' => [ + 'aField' => '', + ], + 'processedTca' => [ + 'columns' => [ + 'aField' => [ + 'config' => [ + 'type' => 'uuid', + ], + ], + ], + ], + ]; + + $input['processedTca']['columns']['aField']['config']['version'] = 6; + self::assertEquals(6, (int)(new TcaUuid())->addData($input)['databaseRow']['aField'][14]); + + $input['processedTca']['columns']['aField']['config']['version'] = 7; + self::assertEquals(7, (int)(new TcaUuid())->addData($input)['databaseRow']['aField'][14]); + + $input['processedTca']['columns']['aField']['config']['version'] = 4; + self::assertEquals(4, (int)(new TcaUuid())->addData($input)['databaseRow']['aField'][14]); + + $input['processedTca']['columns']['aField']['config']['version'] = 12345678; // Defaults to 4 + self::assertEquals(4, (int)(new TcaUuid())->addData($input)['databaseRow']['aField'][14]); + + unset($input['processedTca']['columns']['aField']['config']['version']); // Defaults to 4 + self::assertEquals(4, (int)(new TcaUuid())->addData($input)['databaseRow']['aField'][14]); + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index e5e1b0c41498e692800479312999bcaf91eda019..a151848ae4195278f63d4d23958df11f9cb6d074 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -21,6 +21,7 @@ use Doctrine\DBAL\Types\IntegerType; use Doctrine\DBAL\Types\JsonType; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Uid\Uuid; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Cache\CacheManager; @@ -1503,6 +1504,7 @@ class DataHandler implements LoggerAwareInterface 'text' => $this->checkValueForText($value, $tcaFieldConf, $table, $realPid, $field), 'group', 'folder', 'select' => $this->checkValueForGroupFolderSelect($res, $value, $tcaFieldConf, $table, $id, $status, $field), 'json' => $this->checkValueForJson($value, $tcaFieldConf), + 'uuid' => $this->checkValueForUuid((string)$value, $tcaFieldConf), 'passthrough', 'imageManipulation', 'user' => ['value' => $value], default => [], }; @@ -2385,6 +2387,32 @@ class DataHandler implements LoggerAwareInterface return $res; } + /** + * Evaluate "uuid" type values. Will create a new uuid in case + * an invalid uuid is provided and the field is marked as required. + * + * @param string $value The value to set. + * @param array $tcaFieldConf Field configuration from TCA + * + * @return array $res The result array. The processed value (if any!) is set in the "value" key. + */ + protected function checkValueForUuid(string $value, array $tcaFieldConf): array + { + if (Uuid::isValid($value)) { + return ['value' => $value]; + } + + if ($tcaFieldConf['required'] ?? true) { + return ['value' => (string)match ((int)($tcaFieldConf['version'] ?? 0)) { + 6 => Uuid::v6(), + 7 => Uuid::v7(), + default => Uuid::v4() + }]; + } + // Unset invalid uuid - in case a field value is not required + return []; + } + /** * Applies the filter methods from a column's TCA configuration to a value array. * diff --git a/typo3/sysext/core/Classes/DataHandling/TableColumnType.php b/typo3/sysext/core/Classes/DataHandling/TableColumnType.php index be263688ba137d85e9ae3e397cdaf66015d7533c..a9bc50544221911366b45f34ac49aaca30252c95 100644 --- a/typo3/sysext/core/Classes/DataHandling/TableColumnType.php +++ b/typo3/sysext/core/Classes/DataHandling/TableColumnType.php @@ -51,6 +51,7 @@ final class TableColumnType extends Enumeration public const NUMBER = 'NUMBER'; public const FILE = 'FILE'; public const JSON = 'JSON'; + public const UUID = 'UUID'; /** * @param mixed $type diff --git a/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php b/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php index afb78963f107bcfe036c9e2ef16cae4f5f79480e..98bb4bc45234669e3f3885a15cde2fd5b7b2d6cb 100644 --- a/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php +++ b/typo3/sysext/core/Classes/Database/Schema/DefaultTcaSchema.php @@ -521,6 +521,25 @@ class DefaultTcaSchema ] ); } + + // Add uuid fields for all tables, defining uuid columns (TCA type=uuid) + foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) { + if ((string)($fieldConfig['config']['type'] ?? '') !== 'uuid' + || $this->isColumnDefinedForTable($tables, $tableName, $fieldName) + ) { + continue; + } + + $tables[$tablePosition]->addColumn( + $this->quote($fieldName), + 'string', + [ + 'length' => 36, + 'default' => '', + 'notnull' => true, + ] + ); + } } return $tables; diff --git a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php index 8dcc458a1962113a2baec211ed84ddb1c109c139..fd63a377c8410a97609b82679c2641ab4730f146 100644 --- a/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php +++ b/typo3/sysext/core/Classes/Resource/Search/QueryRestrictions/SearchTermRestriction.php @@ -113,6 +113,7 @@ class SearchTermRestriction implements QueryRestrictionInterface || $fieldType === 'link' || $fieldType === 'color' || $fieldType === 'input' + || $fieldType === 'uuid' ) { $constraintsForParts[] = $searchConstraint; } diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index ed54d2a618c6c6480d2d6988b8dc0ede846c6868..15beb76f3c8991beffb9cbe9c99c67509dfa8ec8 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -604,10 +604,15 @@ return [ \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class, ], ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaUuid::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class, + ], + ], \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [ 'depends' => [ \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class, - \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class, + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaUuid::class, ], ], \TYPO3\CMS\Backend\Form\FormDataProvider\TcaCheckboxItems::class => [ @@ -854,6 +859,11 @@ return [ \TYPO3\CMS\Backend\Form\FormDataProvider\TcaText::class, ], ], + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaUuid::class => [ + 'depends' => [ + \TYPO3\CMS\Backend\Form\FormDataProvider\TcaJson::class, + ], + ], \TYPO3\CMS\Backend\Form\FormDataProvider\TcaRadioItems::class => [ 'depends' => [ \TYPO3\CMS\Backend\Form\FormDataProvider\SiteResolving::class, diff --git a/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100171-IntroduceTCATypeUuid.rst b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100171-IntroduceTCATypeUuid.rst new file mode 100644 index 0000000000000000000000000000000000000000..15c6707fd30726c728ddfbc8415d4d2ac59fc8e5 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.3/Feature-100171-IntroduceTCATypeUuid.rst @@ -0,0 +1,65 @@ +.. include:: /Includes.rst.txt + +.. _feature-100171-1678869689: + +========================================== +Feature: #100171 - Introduce TCA type uuid +========================================== + +See :issue:`100171` + +Description +=========== + +In our effort of introducing dedicated TCA types for special use cases, +a new TCA field type called :php:`uuid` has been added to TYPO3 Core. +Its main purpose is to simplify the TCA configuration when working with +fields, containing a UUID. + +The TCA type :php:`uuid` features the following column configuration: + +- :php:`enableCopyToClipboard` +- :php:`fieldInformation` +- :php:`required`: Defaults to :php:`true` +- :php:`size` +- :php:`version` + +.. note:: + + In case :php:`enableCopyToClipboard` is set to :php:`true`, which is the + default, a button is rendered next to the input field, which allows to copy + the UUID to the operating system's clipboard. + +.. note:: + + The :php:`version` option defines the uuid version to be used. Allowed + values are `4`, `6` or `7`. The default is `4`. For more information + about the different versions, have a look at the corresponding + `symfony documentation`_. + +The following column configuration can be overwritten by Page TSconfig: + +- :typoscript:`size` +- :typoscript:`enableCopyToClipboard` + +An example configuration looks like the following: + +.. code-block:: php + + 'identifier' => [ + 'label' => 'My record identifier', + 'config' => [ + 'type' => 'uuid', + 'version' => 6, + ], + ], + +Impact +====== + +It's now possible to use a dedicated TCA type for rendering of a UUID field. +Using the new TCA type, corresponding database columns are added automatically. + +.. _symfony documentation: https://symfony.com/doc/current/components/uid.html#uuids + +.. index:: Backend, TCA, ext:backend diff --git a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php index 734abd345c949274bda52ba4d2271d698beefeb8..40bba7ee3113559df1c34dff6cf2611d1bd7e7b8 100644 --- a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php +++ b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Tests\Unit\DataHandling; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Uid\Uuid; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\NullFrontend; @@ -1095,6 +1096,49 @@ class DataHandlerTest extends UnitTestCase ); } + /** + * @test + */ + public function checkValueForUuidReturnsValidUuidUnmodified(): void + { + self::assertEquals( + 'b3190536-1431-453e-afbb-25b8c5022513', + Uuid::isValid($this->subject->_call('checkValueForUuid', 'b3190536-1431-453e-afbb-25b8c5022513', [])['value']) + ); + } + + /** + * @test + */ + public function checkValueForUuidCreatesValidUuidValueForReqiredFieldsWithInvalidUuidGiven(): void + { + self::assertTrue(Uuid::isValid($this->subject->_call('checkValueForUuid', '', [])['value'])); + self::assertTrue(Uuid::isValid($this->subject->_call('checkValueForUuid', '-_invalid_-', [])['value'])); + } + + /** + * @test + */ + public function checkValueForUuidDiscardsInvalidUuidIfFieldIsNotRequired(): void + { + self::assertEmpty($this->subject->_call('checkValueForUuid', '', ['required' => false])); + self::assertEmpty($this->subject->_call('checkValueForUuid', '-_invalid_-', ['required' => false])); + } + + /** + * @test + */ + public function checkValueForUuidCreatesValidUuidValueWithDefinedVersion(): void + { + self::assertEquals(6, (int)$this->subject->_call('checkValueForUuid', '', ['version' => 6])['value'][14]); + self::assertEquals(7, (int)$this->subject->_call('checkValueForUuid', '', ['version' => 7])['value'][14]); + self::assertEquals(4, (int)$this->subject->_call('checkValueForUuid', '', ['version' => 4])['value'][14]); + // Defaults to 4 + self::assertEquals(4, (int)$this->subject->_call('checkValueForUuid', '', ['version' => 123456678])['value'][14]); + // Defaults to 4 + self::assertEquals(4, (int)$this->subject->_call('checkValueForUuid', '', [])['value'][14]); + } + /** * @test * @dataProvider referenceValuesAreCastedDataProvider diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json index 6127790262e751c5dcfea1e156c1c7729b42f72b..868bbd292b0a067bf2cf8cecce29a5b9c68f3639 100644 --- a/typo3/sysext/core/composer.json +++ b/typo3/sysext/core/composer.json @@ -68,6 +68,7 @@ "symfony/options-resolver": "^6.2", "symfony/rate-limiter": "^6.2", "symfony/routing": "^6.2", + "symfony/uid": "^6.2", "symfony/yaml": "^6.2", "typo3/class-alias-loader": "^1.1.4", "typo3/cms-cli": "^3.1", diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php index a9d19203f5f80ddd39236da2e5b153c360e64186..27f24a5244d8e63f059318890151a34ad6e48ddd 100644 --- a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php +++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Mapper/DataMapFactoryTest.php @@ -464,6 +464,7 @@ class DataMapFactoryTest extends UnitTestCase [['type' => 'number'], TableColumnType::NUMBER], [['type' => 'file'], TableColumnType::FILE], [['type' => 'json'], TableColumnType::JSON], + [['type' => 'uuid'], TableColumnType::UUID], ]; } diff --git a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php index cc0f5a57e211920ccdfd6336cdcc62e6f5447fc8..fe1014e76377755c6f9f776e3def88c37fcc5897 100644 --- a/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php +++ b/typo3/sysext/lowlevel/Classes/Controller/DatabaseIntegrityController.php @@ -922,6 +922,7 @@ class DatabaseIntegrityController case 'password': case 'color': case 'json': + case 'uuid': default: $fields['type'] = 'text'; } @@ -2257,6 +2258,7 @@ class DatabaseIntegrityController case 'password': case 'color': case 'json': + case 'uuid': default: $this->fields[$fieldName]['type'] = 'text'; } diff --git a/typo3/sysext/reactions/Classes/Form/Element/UuidElement.php b/typo3/sysext/reactions/Classes/Form/Element/UuidElement.php deleted file mode 100644 index f9298e839bc0a0f7e27e37b381529236e2d5ead3..0000000000000000000000000000000000000000 --- a/typo3/sysext/reactions/Classes/Form/Element/UuidElement.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Reactions\Form\Element; - -use Symfony\Component\Uid\Uuid; -use TYPO3\CMS\Backend\Form\Element\AbstractFormElement; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Utility\StringUtility; - -/** - * Creates a readonly input element with a UUID. - * - * This is rendered for config type=user, renderType=uuid - * - * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API. - */ -class UuidElement extends AbstractFormElement -{ - /** - * Default field information enabled for this element. - * - * @var array - */ - protected $defaultFieldInformation = [ - 'tcaDescription' => [ - 'renderType' => 'tcaDescription', - ], - ]; - - public function render() - { - $resultArray = $this->initializeResultArray(); - $parameterArray = $this->data['parameterArray']; - $itemValue = $parameterArray['itemFormElValue'] ?: (string)Uuid::v4(); - $fieldId = StringUtility::getUniqueId('formengine-input-'); - - $attributes = [ - 'id' => $fieldId, - 'name' => htmlspecialchars($parameterArray['itemFormElName']), - 'size' => 40, - 'class' => 'form-control', - 'data-formengine-input-name' => htmlspecialchars($parameterArray['itemFormElName']), - ]; - - $fieldInformationResult = $this->renderFieldInformation(); - $fieldInformationHtml = $fieldInformationResult['html']; - $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false); - - $html = []; - $html[] = '<div class="formengine-field-item t3js-formengine-field-item">'; - $html[] = $fieldInformationHtml; - $html[] = '<div class="form-control-wrap" style="max-width: ' . $this->formMaxWidth($this->defaultInputWidth) . 'px">'; - $html[] = '<div class="form-wizards-wrap">'; - $html[] = '<div class="form-wizards-element">'; - $html[] = '<input type="text" readonly="readonly" disabled="disabled" value="' . htmlspecialchars($itemValue, ENT_QUOTES) . '" '; - $html[] = GeneralUtility::implodeAttributes($attributes, true); - $html[] = '/>'; - $html[] = '</div>'; - $html[] = '</div>'; - $html[] = '</div>'; - $html[] = '</div>'; - $resultArray['html'] = implode(LF, $html); - return $resultArray; - } -} diff --git a/typo3/sysext/reactions/Classes/Hooks/DataHandlerHook.php b/typo3/sysext/reactions/Classes/Hooks/DataHandlerHook.php deleted file mode 100644 index 46abba94c4848812f5dc720d7761d51a7b2ccf08..0000000000000000000000000000000000000000 --- a/typo3/sysext/reactions/Classes/Hooks/DataHandlerHook.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -namespace TYPO3\CMS\Reactions\Hooks; - -use Symfony\Component\Uid\Uuid; -use TYPO3\CMS\Core\DataHandling\DataHandler; - -/** - * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API. - */ -class DataHandlerHook -{ - public function processDatamap_postProcessFieldArray($status, $table, $id, array &$fieldArray, DataHandler $dataHandler): void - { - // Only consider reactions - if ($table !== 'sys_reaction') { - return; - } - // Only consider new reactions - if ($status !== 'new') { - return; - } - // Create a UUID for a new reaction if non is present in the field array - if (!isset($fieldArray['identifier'])) { - $fieldArray['identifier'] = (string)Uuid::v4(); - } - // Create a valid UUID for a new reaction if given identifier is invalid - if (!Uuid::isValid($fieldArray['identifier'])) { - $fieldArray['identifier'] = (string)Uuid::v4(); - } - } -} diff --git a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php index 7c3b5efd8817da543635fd9a34412fdcb15f3244..04fbc47c4fdb44b9d530015e072455b976f4df21 100644 --- a/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php +++ b/typo3/sysext/reactions/Configuration/TCA/sys_reaction.php @@ -80,8 +80,7 @@ return [ 'label' => 'LLL:EXT:reactions/Resources/Private/Language/locallang_db.xlf:sys_reaction.identifier', 'description' => 'LLL:EXT:reactions/Resources/Private/Language/locallang_db.xlf:sys_reaction.identifier.description', 'config' => [ - 'type' => 'user', - 'renderType' => 'uuid', + 'type' => 'uuid', ], ], 'secret' => [ diff --git a/typo3/sysext/reactions/composer.json b/typo3/sysext/reactions/composer.json index fcd929e598db34603be2602b002fa2259db2b87d..70f8b320a88c837368d84446571a7ed6570b2a97 100644 --- a/typo3/sysext/reactions/composer.json +++ b/typo3/sysext/reactions/composer.json @@ -19,7 +19,6 @@ "sort-packages": true }, "require": { - "symfony/uid": "^6.2", "typo3/cms-core": "12.3.*@dev" }, "suggest": { diff --git a/typo3/sysext/reactions/ext_localconf.php b/typo3/sysext/reactions/ext_localconf.php index b25163e51c8b97418f2ee2847733838dae81287f..72c7e317f5dc4e75b5feb2879843505268f3d5a0 100644 --- a/typo3/sysext/reactions/ext_localconf.php +++ b/typo3/sysext/reactions/ext_localconf.php @@ -3,8 +3,6 @@ declare(strict_types=1); use TYPO3\CMS\Reactions\Form\Element\FieldMapElement; -use TYPO3\CMS\Reactions\Form\Element\UuidElement; -use TYPO3\CMS\Reactions\Hooks\DataHandlerHook; defined('TYPO3') or die(); @@ -13,12 +11,3 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1660911089] = [ 'priority' => 40, 'class' => FieldMapElement::class, ]; - -// @todo This should be a dedicated TCA type instead -$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1660911009] = [ - 'nodeName' => 'uuid', - 'priority' => 40, - 'class' => UuidElement::class, -]; - -$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = DataHandlerHook::class; diff --git a/typo3/sysext/reactions/ext_tables.sql b/typo3/sysext/reactions/ext_tables.sql index 87fc55ea9df2d1ec48804e0a3efa8598b91d96eb..3de1a2af69ea4397a348b5276d4c6c02dc354e0b 100644 --- a/typo3/sysext/reactions/ext_tables.sql +++ b/typo3/sysext/reactions/ext_tables.sql @@ -4,7 +4,6 @@ CREATE TABLE sys_reaction ( name varchar(100) DEFAULT '' NOT NULL, reaction_type varchar(255) DEFAULT '' NOT NULL, - identifier varchar(36) DEFAULT '' NOT NULL, secret varchar(255) DEFAULT '' NOT NULL, impersonate_user int(11) unsigned DEFAULT '0' NOT NULL, table_name varchar(255) DEFAULT '' NOT NULL,