[BUGFIX] Resolve correct page in slug validation 29/58229/8
authorOliver Hader <oliver@typo3.org>
Sat, 8 Sep 2018 12:54:45 +0000 (14:54 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Sat, 27 Oct 2018 20:37:55 +0000 (22:37 +0200)
The SlugHelper now receives an encapsulated RecordState object that
represents a record.

This allows fine-grained control over a record and helps resolving
related information, which is required to resolve slugs properly in a
case where e.g. the node ("parent") and language uid can occur multiple
times.

The RecordState contains:

- an EntityContext which describes a variant of a record by its language
  and workspace assignment

- a node object (EntityPointer) that points to the node (aka "parent") of
  the record

- a EntityUidPointer that describes the origin of the record by its table
  name and uid

The RecordStateFactory creates such RecordState objects and enriches them
with links (EntityPointerLink) that point to languages and versions, that
are also represented by EntityPointer implementations.

Resolves: #86195
Releases: master
Change-Id: If17a30e98f802825d80e95044572153f2426bea2
Reviewed-on: https://review.typo3.org/58229
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Andreas Wolf <andreas.wolf@typo3.org>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/backend/Classes/Controller/FormSlugAjaxController.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/DataHandling/Model/EntityContext.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Model/EntityPointer.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Model/EntityPointerLink.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Model/EntityUidPointer.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Model/RecordState.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/Model/RecordStateFactory.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/SlugHelper.php
typo3/sysext/install/Classes/Updates/PopulatePageSlugs.php

index 435664e..04cb72e 100644 (file)
@@ -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
     {
index 05ebd41..0f2e0f9 100644 (file)
@@ -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 (file)
index 0000000..582a918
--- /dev/null
@@ -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 (file)
index 0000000..a8b2099
--- /dev/null
@@ -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 (file)
index 0000000..92417f0
--- /dev/null
@@ -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 (file)
index 0000000..1032b12
--- /dev/null
@@ -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 (file)
index 0000000..e7a0935
--- /dev/null
@@ -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 (file)
index 0000000..1d46225
--- /dev/null
@@ -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
+        );
+    }
+}
index 30b8bd0..86cba5b 100644 (file)
@@ -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);
index f344797..dd61de3 100644 (file)
@@ -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(