diff --git a/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php index 4b76063326c87d25fd39e66b11a97b0c0a126412..4d8c992ba7af10051c25fd062d6d2ef00b10de14 100644 --- a/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php +++ b/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php @@ -90,6 +90,7 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController } $evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : []; + $hasToBeUniqueInDb = in_array('unique', $evalInfo, true); $hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true); $hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true); @@ -119,6 +120,10 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController $state = RecordStateFactory::forName($tableName) ->fromArray($recordData, $pid, $recordId); + if ($hasToBeUniqueInDb && !$slug->isUniqueInTable($proposal, $state)) { + $hasConflict = true; + $proposal = $slug->buildSlugForUniqueInTable($proposal, $state); + } if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $state)) { $hasConflict = true; $proposal = $slug->buildSlugForUniqueInSite($proposal, $state); diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 1e630708148ac6c6427106f66b7dde5516de324b..ae05c7bb94a141c2d21961e4188c373872ee210c 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -1945,6 +1945,9 @@ class DataHandler implements LoggerAwareInterface $state = RecordStateFactory::forName($table) ->fromArray($fullRecord, $realPid, $id); $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true); + if (in_array('unique', $evalCodesArray, true)) { + $value = $helper->buildSlugForUniqueInTable($value, $state); + } if (in_array('uniqueInSite', $evalCodesArray, true)) { $value = $helper->buildSlugForUniqueInSite($value, $state); } diff --git a/typo3/sysext/core/Classes/DataHandling/SlugHelper.php b/typo3/sysext/core/Classes/DataHandling/SlugHelper.php index 2e5914fef08dc90d7fff68c0636a6e1980ddf573..6d13acb511c648054645bb1faa57b940288871d4 100644 --- a/typo3/sysext/core/Classes/DataHandling/SlugHelper.php +++ b/typo3/sysext/core/Classes/DataHandling/SlugHelper.php @@ -325,6 +325,31 @@ class SlugHelper return true; } + /** + * Check if there are other records with the same slug. + * + * @param string $slug + * @param RecordState $state + * @return bool + * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException + */ + public function isUniqueInTable(string $slug, RecordState $state): bool + { + $languageId = $state->getContext()->getLanguageId(); + + $queryBuilder = $this->createPreparedQueryBuilder(); + $this->applySlugConstraint($queryBuilder, $slug); + $this->applyLanguageConstraint($queryBuilder, $languageId); + $this->applyWorkspaceConstraint($queryBuilder, $state); + $statement = $queryBuilder->execute(); + + $records = $this->resolveVersionOverlays( + $statement->fetchAll() + ); + + return count($records) === 0; + } + /** * Ensure root line caches are flushed to avoid any issue regarding moving of pages or dynamically creating * sites while managing slugs at the same request @@ -388,6 +413,33 @@ class SlugHelper return $newValue; } + /** + * Generate a slug with a suffix "/mytitle-1" if that is in use already. + * + * @param string $slug proposed slug + * @param RecordState $state + * @return string + * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException + */ + public function buildSlugForUniqueInTable(string $slug, RecordState $state): string + { + $slug = $this->sanitize($slug); + $rawValue = $this->extract($slug); + $newValue = $slug; + $counter = 0; + while (!$this->isUniqueInTable( + $newValue, + $state + ) && $counter++ < 100 + ) { + $newValue = $this->sanitize($rawValue . '-' . $counter); + } + if ($counter === 100) { + $newValue = $this->sanitize($rawValue . '-' . GeneralUtility::shortMD5($rawValue)); + } + return $newValue; + } + /** * @return QueryBuilder */ diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueBase.csv b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueBase.csv new file mode 100644 index 0000000000000000000000000000000000000000..e91342d5c0dd60beed7d86fc49db74e30dc21fd9 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueBase.csv @@ -0,0 +1,5 @@ +"pages",,,, +,"uid","pid","title","slug" +,1,0,"root","/" +,2,1,"Page One","/page-one" +,3,1,"Page Two","/page-two" diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueResult.csv b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueResult.csv new file mode 100644 index 0000000000000000000000000000000000000000..090cffb65d59c309c8914b9902e3aa09545d0581 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueResult.csv @@ -0,0 +1,5 @@ +"pages",,,, +,"uid","pid","title","slug" +,1,0,"root","/" +,2,1,"Page One","/page-one" +,3,1,"Page One","/page-one-1" diff --git a/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/SlugUniqueTest.php b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/SlugUniqueTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fbecb711aa829ecfca4b4ba20e86b04eb53917d9 --- /dev/null +++ b/typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/SlugUniqueTest.php @@ -0,0 +1,74 @@ +<?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\Core\Tests\Functional\DataHandling\DataHandler; + +use TYPO3\CMS\Core\DataHandling\DataHandler; +use TYPO3\CMS\Core\Tests\Functional\DataHandling\AbstractDataHandlerActionTestCase; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Tests related to DataHandler slug unique handling + */ +class SlugUniqueTest extends AbstractDataHandlerActionTestCase +{ + protected function setUp(): void + { + parent::setUp(); + $this->setUpFrontendSite(1); + $this->importCSVDataSet(__DIR__ . '/DataSet/TestSlugUniqueBase.csv'); + } + + /** + * Data provider for differentUniqueEvalSettingsDeDuplicateSlug + * @return array + */ + public function getEvalSettingDataProvider(): array + { + return [ + 'uniqueInSite' => ['uniqueInSite'], + 'unique' => ['unique'], + 'uniqueInPid' => ['uniqueInPid'], + ]; + } + + /** + * @dataProvider getEvalSettingDataProvider + * @test + * @param string $uniqueSetting + */ + public function differentUniqueEvalSettingsDeDuplicateSlug(string $uniqueSetting) + { + $GLOBALS['TCA']['pages']['columns']['slug']['config']['eval'] = $uniqueSetting; + $dataHandler = GeneralUtility::makeInstance(DataHandler::class); + $dataHandler->enableLogging = false; + $dataHandler->start( + [ + 'pages' => [ + 3 => [ + 'title' => 'Page One', + 'slug' => 'page-one', + ], + ], + ], + [] + ); + $dataHandler->process_datamap(); + + $this->assertCSVDataSet('typo3/sysext/core/Tests/Functional/DataHandling/DataHandler/DataSet/TestSlugUniqueResult.csv'); + } +}