diff --git a/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php b/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php index 435664e2cd8548f6f5242a9e1108b295eaeda5b2..04cb72e32942693590bdd27a6d25fb6befc07023 100644 --- a/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php +++ b/typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Backend\Controller; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory; use TYPO3\CMS\Core\DataHandling\SlugHelper; use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -55,6 +56,7 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController * * @param ServerRequestInterface $request * @return ResponseInterface + * @throws \RuntimeException */ public function suggestAction(ServerRequestInterface $request): ResponseInterface { @@ -84,17 +86,17 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController $hasConflict = false; + $recordData = $values; + $recordData['pid'] = $pid; + if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) { + $recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId; + } + $slug = GeneralUtility::makeInstance(SlugHelper::class, $tableName, $fieldName, $fieldConfig); if ($mode === 'auto') { // New page - Feed incoming values to generator - $recordData = $values; - $recordData['pid'] = $pid; - $recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId; $proposal = $slug->generate($recordData, $pid); } elseif ($mode === 'recreate') { - $recordData = $values; - $recordData['pid'] = $pid; - $recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId; $proposal = $slug->generate($recordData, $parentPageId); } elseif ($mode === 'manual') { // Existing record - Fetch full record and only validate against the new "slug" field. @@ -103,13 +105,15 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController throw new \RuntimeException('mode must be either "auto", "recreate" or "manual"', 1535835666); } - if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $recordId, $pid, $languageId)) { + $state = RecordStateFactory::forName($tableName) + ->fromArray($recordData, $pid, $recordId); + if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $state)) { $hasConflict = true; - $proposal = $slug->buildSlugForUniqueInSite($proposal, $recordId, $pid, $languageId); + $proposal = $slug->buildSlugForUniqueInSite($proposal, $state); } - if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $recordId, $pid, $languageId)) { + if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $state)) { $hasConflict = true; - $proposal = $slug->buildSlugForUniqueInPid($proposal, $recordId, $pid, $languageId); + $proposal = $slug->buildSlugForUniqueInPid($proposal, $state); } return new JsonResponse([ @@ -122,6 +126,7 @@ class FormSlugAjaxController extends AbstractFormEngineAjaxController /** * @param ServerRequestInterface $request * @return bool + * @throws \InvalidArgumentException */ protected function checkRequest(ServerRequestInterface $request): bool { diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 05ebd414309354f872c349328a05bce616325658..0f2e0f9fed0e9d1de40a6bf49fb9ab722648326e 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -47,6 +47,7 @@ use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Database\RelationHandler; use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore; use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor; +use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory; use TYPO3\CMS\Core\Html\RteHtmlParser; use TYPO3\CMS\Core\Localization\LanguageService; use TYPO3\CMS\Core\Messaging\FlashMessage; @@ -1976,9 +1977,6 @@ class DataHandler implements LoggerAwareInterface $value = $helper->sanitize($value); } - $languageId = (int)$fullRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; - $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true); - // In case a workspace is given, and the $realPid(!) still is negative // this is most probably triggered by versionizeRecord() and a raw record // copy - thus, uniqueness cannot be determined without having the @@ -1990,11 +1988,19 @@ class DataHandler implements LoggerAwareInterface return ['value' => $value]; } + // Return directly in case no evaluations are defined + if (empty($tcaFieldConf['eval'])) { + return ['value' => $value]; + } + + $state = RecordStateFactory::forName($table) + ->fromArray($fullRecord, $realPid, $id); + $evalCodesArray = GeneralUtility::trimExplode(',', $tcaFieldConf['eval'], true); if (in_array('uniqueInSite', $evalCodesArray, true)) { - $value = $helper->buildSlugForUniqueInSite($value, $id, $realPid, $languageId); + $value = $helper->buildSlugForUniqueInSite($value, $state); } if (in_array('uniqueInPid', $evalCodesArray, true)) { - $value = $helper->buildSlugForUniqueInPid($value, $id, $realPid, $languageId); + $value = $helper->buildSlugForUniqueInPid($value, $state); } return ['value' => $value]; diff --git a/typo3/sysext/core/Classes/DataHandling/Model/EntityContext.php b/typo3/sysext/core/Classes/DataHandling/Model/EntityContext.php new file mode 100644 index 0000000000000000000000000000000000000000..582a9183fac53d732e2baa9cdc82795fd759e861 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/EntityContext.php @@ -0,0 +1,80 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +/** + * Represents the context of an entity + * + * A context defines a "variant" of an entity, currently by its language and workspace assignment. The EntityContext + * is bound to a RecordState. + */ +class EntityContext +{ + /** + * @var int + */ + protected $workspaceId = 0; + + /** + * @var int + */ + protected $languageId = 0; + + /** + * @return int + */ + public function getWorkspaceId(): int + { + return $this->workspaceId; + } + + /** + * @param int $workspaceId + * @return static + */ + public function withWorkspaceId(int $workspaceId): self + { + if ($this->workspaceId === $workspaceId) { + return $this; + } + $target = clone $this; + $target->workspaceId = $workspaceId; + return $target; + } + + /** + * @return int + */ + public function getLanguageId(): int + { + return $this->languageId; + } + + /** + * @param int $languageId + * @return static + */ + public function withLanguageId(int $languageId): self + { + if ($this->languageId === $languageId) { + return $this; + } + $target = clone $this; + $target->languageId = $languageId; + return $target; + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/Model/EntityPointer.php b/typo3/sysext/core/Classes/DataHandling/Model/EntityPointer.php new file mode 100644 index 0000000000000000000000000000000000000000..a8b20995a5798eec6a5ee00c5683650f571e4b5e --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/EntityPointer.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +/** + * Interface describing pointers to an entity + */ +interface EntityPointer +{ + /** + * @return string + */ + public function getName(): string; + + /** + * @return string + */ + public function getIdentifier(): string; + + /** + * @return bool + */ + public function isNode(): bool; + + /** + * @param EntityPointer $other + * @return bool + */ + public function isEqualTo(EntityPointer $other): bool; +} diff --git a/typo3/sysext/core/Classes/DataHandling/Model/EntityPointerLink.php b/typo3/sysext/core/Classes/DataHandling/Model/EntityPointerLink.php new file mode 100644 index 0000000000000000000000000000000000000000..92417f0c2b4ffde6b2e464f7b416f2276e6d0e8a --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/EntityPointerLink.php @@ -0,0 +1,83 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +/** + * An EntityPointerLink is used to connect EntityPointer instances + */ +class EntityPointerLink +{ + /** + * @var EntityPointer + */ + protected $subject; + + /** + * @var EntityPointerLink|null + */ + protected $ancestor; + + /** + * @param EntityPointer $subject + */ + public function __construct(EntityPointer $subject) + { + $this->subject = $subject; + } + + /** + * @return EntityPointer + */ + public function getSubject(): EntityPointer + { + return $this->subject; + } + + /** + * @return EntityPointerLink + */ + public function getHead(): EntityPointerLink + { + $head = $this; + while ($head->ancestor !== null) { + $head = $head->ancestor; + } + return $head; + } + + /** + * @return EntityPointerLink|null + */ + public function getAncestor(): ?EntityPointerLink + { + return $this->ancestor; + } + + /** + * @param EntityPointerLink $ancestor + * @return EntityPointerLink + */ + public function withAncestor(EntityPointerLink $ancestor): self + { + if ($this->ancestor === $ancestor) { + return $this; + } + $target = clone $this; + $target->ancestor = $ancestor; + return $target; + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/Model/EntityUidPointer.php b/typo3/sysext/core/Classes/DataHandling/Model/EntityUidPointer.php new file mode 100644 index 0000000000000000000000000000000000000000..1032b123066402b071966d2ca17561d422c36d38 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/EntityUidPointer.php @@ -0,0 +1,91 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +/** + * The EntityUidPointer represents the concrete origin of the entity + */ +class EntityUidPointer implements EntityPointer +{ + /** + * @var string + */ + protected $name; + + /** + * @var string + */ + protected $identifier; + + /** + * @param string $name + * @param string $identifier + */ + public function __construct(string $name, string $identifier) + { + $this->name = $name; + $this->identifier = $identifier; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @param string $identifier + * @return static + */ + public function withUid(string $identifier): self + { + if ($this->identifier === $identifier) { + return $this; + } + $target = clone $this; + $target->identifier = $identifier; + return $target; + } + + /** + * @return bool + */ + public function isNode(): bool + { + return $this->name === 'pages'; + } + + /** + * @param EntityPointer $other + * @return bool + */ + public function isEqualTo(EntityPointer $other): bool + { + return $this->identifier === $other->getIdentifier() + && $this->name === $other->getName(); + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/Model/RecordState.php b/typo3/sysext/core/Classes/DataHandling/Model/RecordState.php new file mode 100644 index 0000000000000000000000000000000000000000..e7a09358c2f1d01efedb985607b34a02a32d1e99 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/RecordState.php @@ -0,0 +1,164 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Utility\MathUtility; + +/** + * A RecordState is an abstract description of a record that consists of + * + * - an EntityContext describing the "variant" of a record + * - an EntityPointer that describes the node where the record is stored + * - an EntityUidPointer of the record the RecordState instance represents + * + * Instances of this class are created by the RecordStateFactory. + */ +class RecordState +{ + /** + * @var EntityContext + */ + protected $context; + + /** + * @var EntityPointer + */ + protected $node; + + /** + * @var EntityUidPointer + */ + protected $subject; + + /** + * @var EntityPointerLink + */ + protected $languageLink; + + /** + * @var EntityPointerLink + */ + protected $versionLink; + + /** + * @param EntityContext $context + * @param EntityPointer $node + * @param EntityUidPointer $subject + */ + public function __construct(EntityContext $context, EntityPointer $node, EntityUidPointer $subject) + { + $this->context = $context; + $this->node = $node; + $this->subject = $subject; + } + + /** + * @return EntityContext + */ + public function getContext(): EntityContext + { + return $this->context; + } + + /** + * @return EntityPointer + */ + public function getNode(): EntityPointer + { + return $this->node; + } + + /** + * @return EntityUidPointer + */ + public function getSubject(): EntityUidPointer + { + return $this->subject; + } + + /** + * @return EntityPointerLink + */ + public function getLanguageLink(): ?EntityPointerLink + { + return $this->languageLink; + } + + /** + * @param EntityPointerLink|null $languageLink + * @return static + */ + public function withLanguageLink(?EntityPointerLink $languageLink): self + { + if ($this->languageLink === $languageLink) { + return $this; + } + $target = clone $this; + $target->languageLink = $languageLink; + return $target; + } + + /** + * @return EntityPointerLink + */ + public function getVersionLink(): ?EntityPointerLink + { + return $this->versionLink; + } + + /** + * @param EntityPointerLink|null $versionLink + * @return static + */ + public function withVersionLink(?EntityPointerLink $versionLink): self + { + if ($this->versionLink === $versionLink) { + return $this; + } + $target = clone $this; + $target->versionLink = $versionLink; + return $target; + } + + /** + * @return bool + */ + public function isNew(): bool + { + return !MathUtility::canBeInterpretedAsInteger( + $this->subject->getIdentifier() + ); + } + + /** + * Resolve identifier of node used as aggregate. For translated pages + * that would result in the `uid` of the outer-most language parent page. + * + * @return string + */ + public function resolveAggregateNodeIdentifier(): string + { + if ($this->subject->isNode() + && $this->context->getLanguageId() > 0 + && $this->languageLink !== null + ) { + return $this->languageLink->getHead()->getSubject()->getIdentifier(); + } + + return $this->node->getIdentifier(); + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/Model/RecordStateFactory.php b/typo3/sysext/core/Classes/DataHandling/Model/RecordStateFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..1d46225cfb642664d1919c01d5716cdbd27025d5 --- /dev/null +++ b/typo3/sysext/core/Classes/DataHandling/Model/RecordStateFactory.php @@ -0,0 +1,163 @@ +<?php +declare(strict_types = 1); + +namespace TYPO3\CMS\Core\DataHandling\Model; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Factory class that creates a record state + */ +class RecordStateFactory +{ + /** + * @var string + */ + protected $name; + + /** + * @param string $name + * @return static + */ + public static function forName(string $name): self + { + return GeneralUtility::makeInstance( + static::class, + $name + ); + } + + /** + * @param string $name + */ + public function __construct(string $name) + { + $this->name = $name; + } + + /** + * @param array $data + * @param int|string|null $pageId + * @param int|string|null $recordId + * @return object|RecordState + */ + public function fromArray(array $data, $pageId = null, $recordId = null): RecordState + { + $pageId = $pageId ?? $data['pid'] ?? null; + $recordId = $recordId ?? $data['uid'] ?? null; + + $aspectFieldValues = $this->resolveAspectFieldValues($data); + + $context = GeneralUtility::makeInstance(EntityContext::class) + ->withWorkspaceId($aspectFieldValues['workspace']) + ->withLanguageId($aspectFieldValues['language']); + $node = $this->createEntityPointer($pageId, 'pages'); + $subject = $this->createEntityPointer($recordId); + + /** @var RecordState $target */ + $target = GeneralUtility::makeInstance( + RecordState::class, + $context, + $node, + $subject + ); + return $target + ->withLanguageLink($this->resolveLanguageLink($aspectFieldValues)) + ->withVersionLink($this->resolveLanguageLink($aspectFieldValues)); + } + + /** + * @return array + */ + protected function resolveAspectFieldNames(): array + { + return [ + 'workspace' => 't3ver_wsid', + 'versionParent' => 't3ver_oid', + 'language' => $GLOBALS['TCA'][$this->name]['ctrl']['languageField'] ?? null, + 'languageParent' => $GLOBALS['TCA'][$this->name]['ctrl']['transOrigPointerField'] ?? null, + 'languageSource' => $GLOBALS['TCA'][$this->name]['ctrl']['translationSource'] ?? null, + ]; + } + + /** + * @param array $data + * @return array + */ + protected function resolveAspectFieldValues(array $data): array + { + return array_map( + function (string $aspectFieldName) use ($data) { + return (int)($data[$aspectFieldName] ?? 0); + }, + $this->resolveAspectFieldNames() + ); + } + + /** + * @param array $aspectFieldNames + * @return EntityPointerLink|null + */ + protected function resolveLanguageLink(array $aspectFieldNames): ?EntityPointerLink + { + if (!empty($aspectFieldNames['languageSource'])) { + $languageSourceLink = GeneralUtility::makeInstance( + EntityPointerLink::class, + $this->createEntityPointer($aspectFieldNames['languageSource']) + ); + } + + if (!empty($aspectFieldNames['languageParent'])) { + $languageParentLink = GeneralUtility::makeInstance( + EntityPointerLink::class, + $this->createEntityPointer($aspectFieldNames['languageParent']) + ); + } + + if (empty($languageSourceLink) || empty($languageParentLink) + || $languageSourceLink->getSubject()->isEqualTo( + $languageParentLink->getSubject() + ) + ) { + return $languageSourceLink ?? $languageParentLink ?? null; + } + return $languageSourceLink->withAncestor($languageParentLink); + } + + /** + * @param string|int $identifier + * @param string|null $name + * @return EntityPointer + * @throws \LogicException + */ + protected function createEntityPointer($identifier, string $name = null): EntityPointer + { + if ($identifier === null) { + throw new \LogicException( + 'Cannot create null pointer', + 1536407967 + ); + } + + $identifier = (string)$identifier; + + return GeneralUtility::makeInstance( + EntityUidPointer::class, + $name ?? $this->name, + $identifier + ); + } +} diff --git a/typo3/sysext/core/Classes/DataHandling/SlugHelper.php b/typo3/sysext/core/Classes/DataHandling/SlugHelper.php index 30b8bd0236902334fdd8144373ccf14d5fe92606..86cba5b398f40ad59718ba13c8036dc2aef6dc8a 100644 --- a/typo3/sysext/core/Classes/DataHandling/SlugHelper.php +++ b/typo3/sysext/core/Classes/DataHandling/SlugHelper.php @@ -21,6 +21,8 @@ use TYPO3\CMS\Core\Charset\CharsetConverter; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\DataHandling\Model\RecordState; +use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Routing\SiteMatcher; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; @@ -206,13 +208,15 @@ class SlugHelper * Checks if there are other records with the same slug that are located on the same PID. * * @param string $slug - * @param string|int $recordId - * @param int $pageId - * @param int $languageId + * @param RecordState $state * @return bool */ - public function isUniqueInPid(string $slug, $recordId, int $pageId, int $languageId): bool + public function isUniqueInPid(string $slug, RecordState $state): bool { + $pageId = (int)$state->resolveAggregateNodeIdentifier(); + $recordId = $state->getSubject()->getIdentifier(); + $languageId = $state->getContext()->getLanguageId(); + if ($pageId < 0) { $pageId = $this->resolveLivePageId($recordId); } @@ -235,14 +239,16 @@ class SlugHelper * Check if there are other records with the same slug that are located on the same site. * * @param string $slug - * @param string|int $recordId - * @param int $pageId - * @param int $languageId + * @param RecordState $state * @return bool * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException */ - public function isUniqueInSite(string $slug, $recordId, int $pageId, int $languageId): bool + public function isUniqueInSite(string $slug, RecordState $state): bool { + $pageId = (int)$state->resolveAggregateNodeIdentifier(); + $recordId = $state->getSubject()->getIdentifier(); + $languageId = $state->getContext()->getLanguageId(); + if ($pageId < 0) { $pageId = $this->resolveLivePageId($recordId); } @@ -266,7 +272,13 @@ class SlugHelper $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class); $siteOfCurrentRecord = $siteMatcher->matchByPageId($pageId); foreach ($records as $record) { - $siteOfExistingRecord = $siteMatcher->matchByPageId((int)$record['uid']); + try { + $siteOfExistingRecord = $siteMatcher->matchByPageId((int)$record['uid']); + } catch (SiteNotFoundException $exception) { + // In case not site is found, the record is not + // organized in any site or pseudo-site + continue; + } if ($siteOfExistingRecord->getRootPageId() === $siteOfCurrentRecord->getRootPageId()) { return false; } @@ -280,13 +292,11 @@ class SlugHelper * Generate a slug with a suffix "/mytitle-1" if that is in use already. * * @param string $slug proposed slug - * @param mixed $recordId can be a new record (non-int) or an existing record ID - * @param int $realPid pageID (already workspace-resolved) - * @param int $languageId the language ID realm to be searched for + * @param RecordState $state * @return string * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException */ - public function buildSlugForUniqueInSite(string $slug, $recordId, int $realPid, int $languageId): string + public function buildSlugForUniqueInSite(string $slug, RecordState $state): string { $slug = $this->sanitize($slug); $rawValue = $this->extract($slug); @@ -294,9 +304,7 @@ class SlugHelper $counter = 0; while (!$this->isUniqueInSite( $newValue, - $recordId, - $realPid, - $languageId + $state ) && $counter++ < 100 ) { $newValue = $this->sanitize($rawValue . '-' . $counter); @@ -311,12 +319,10 @@ class SlugHelper * Generate a slug with a suffix "/mytitle-1" if the suggested slug is in use already. * * @param string $slug proposed slug - * @param mixed $recordId can be a new record (non-int) or an existing record ID - * @param int $realPid pageID (already workspace-resolved) - * @param int $languageId the language ID realm to be searched for + * @param RecordState $state * @return string */ - public function buildSlugForUniqueInPid(string $slug, $recordId, int $realPid, int $languageId): string + public function buildSlugForUniqueInPid(string $slug, RecordState $state): string { $slug = $this->sanitize($slug); $rawValue = $this->extract($slug); @@ -324,9 +330,7 @@ class SlugHelper $counter = 0; while (!$this->isUniqueInPid( $newValue, - $recordId, - $realPid, - $languageId + $state ) && $counter++ < 100 ) { $newValue = $this->sanitize($rawValue . '-' . $counter); diff --git a/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php b/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php index f34479757bfb6709035a6937faad21759eb6a910..dd61de3c1cf8f84f2a2268dc61e5b861c928d350 100644 --- a/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php +++ b/typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php @@ -17,6 +17,7 @@ namespace TYPO3\CMS\Install\Updates; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory; use TYPO3\CMS\Core\DataHandling\SlugHelper; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -166,11 +167,13 @@ class PopulatePageSlugs implements UpgradeWizardInterface $slug = $slugHelper->generate($record, $pid); } - if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $recordId, $pid, $languageId)) { - $slug = $slugHelper->buildSlugForUniqueInSite($slug, $recordId, $pid, $languageId); + $state = RecordStateFactory::forName($this->table) + ->fromArray($record, $pid, $recordId); + if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $state)) { + $slug = $slugHelper->buildSlugForUniqueInSite($slug, $state); } - if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $recordId, $pid, $languageId)) { - $slug = $slugHelper->buildSlugForUniqueInPid($slug, $recordId, $pid, $languageId); + if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $state)) { + $slug = $slugHelper->buildSlugForUniqueInPid($slug, $state); } $connection->update(