From a1f22a427f3bc49ab3fc3424859a7e2d7beaa1c9 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Thu, 29 Apr 2021 15:33:27 +0200
Subject: [PATCH] [BUGFIX] Extbase Relations are resolved properly in
 workspaces

Referencing other records in workspaces usually works in a way
that it is always pointing to the live pendant of a versioned record,
never to the versioned record (t3ver_oid>0). However, for MM this is
the exception - because MM does not have the workspace context.

So when Extbase is resolving a separate query to resolve e.g. a category
from a news record, the MM handling kicks in, but always showed the live
version, because there was no chance to overlay.

Extbase internally first fetches the aggregate root (e.g. news), then does
the overlay and building of the object of the main object, and then fires
another query to fetch e.g. all attached categories / relations. In case
of MM relations for workspaces, they need to be using the versionedUid
of the news record, and not the live version.

However, since this also affects other relations (IRRE without MM,
Select + Group), the RelationHandler is used for workspace'd
relations, and adds one additional query to that.

This change adapts the place where the query for the properties
is built, and takes the UID of the versioned record to resolve the
properties attached to it.

Resolves: #88021
Resolves: #82750
Resolves: #82086
Resolves: #76993
Relates: #57169
Releases: master, 10.4
Change-Id: I57d34b46d17d504301f54071404d6169ec974676
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68913
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Danilo Caccialanza <supercaccia@bluewin.ch>
Tested-by: Martin Tepper <martintepper@arcor.de>
Tested-by: cbugada <christian.bugada@ti.ch>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Danilo Caccialanza <supercaccia@bluewin.ch>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../Persistence/Generic/Mapper/DataMapper.php | 98 ++++++++++++++++++-
 .../Functional/Persistence/WorkspaceTest.php  |  3 +-
 2 files changed, 95 insertions(+), 6 deletions(-)

diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
index a941523f4905..bb5d87f320dd 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Mapper/DataMapper.php
@@ -16,7 +16,9 @@
 namespace TYPO3\CMS\Extbase\Persistence\Generic\Mapper;
 
 use Psr\EventDispatcher\EventDispatcherInterface;
+use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Database\Query\QueryHelper;
+use TYPO3\CMS\Core\Database\RelationHandler;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
 use TYPO3\CMS\Extbase\Event\Persistence\AfterObjectThawedEvent;
@@ -443,13 +445,49 @@ class DataMapper
      */
     protected function getConstraint(QueryInterface $query, DomainObjectInterface $parentObject, $propertyName, $fieldValue = '', $relationTableMatchFields = [])
     {
-        $columnMap = $this->getDataMap(get_class($parentObject))->getColumnMap($propertyName);
-        if ($columnMap->getParentKeyFieldName() !== null) {
-            $constraint = $query->equals($columnMap->getParentKeyFieldName(), $parentObject);
+        $dataMap = $this->getDataMap(get_class($parentObject));
+        $columnMap = $dataMap->getColumnMap($propertyName);
+        $workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id');
+        if ($columnMap && $workspaceId > 0) {
+            $resolvedRelationIds = $this->resolveRelationValuesOfField($dataMap, $columnMap, $parentObject, $fieldValue, $workspaceId);
+        } else {
+            $resolvedRelationIds = [];
+        }
+        // Work with the UIDs directly in a workspace
+        if (!empty($resolvedRelationIds)) {
+            if ($query->getSource() instanceof Persistence\Generic\Qom\JoinInterface) {
+                $constraint = $query->in($query->getSource()->getJoinCondition()->getProperty1Name(), $resolvedRelationIds);
+                // When querying MM relations directly, Typo3DbQueryParser uses enableFields and thus, filters
+                // out versioned records by default. However, we directly query versioned UIDs here, so we want
+                // to include the versioned records explicitly.
+                if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
+                    $query->getQuerySettings()->setEnableFieldsToBeIgnored(['pid']);
+                    $query->getQuerySettings()->setIgnoreEnableFields(true);
+                }
+            } else {
+                $constraint = $query->in('uid', $resolvedRelationIds);
+            }
+            if ($columnMap->getParentTableFieldName() !== null) {
+                $constraint = $query->logicalAnd(
+                    $constraint,
+                    $query->equals($columnMap->getParentTableFieldName(), $dataMap->getTableName())
+                );
+            }
+        } elseif ($columnMap->getParentKeyFieldName() !== null) {
+            $value = $parentObject;
+            // If this a MM relation, and MM relations do not know about workspaces, the MM relations always point to the
+            // versioned record, so this must be taken into account here and the versioned record's UID must be used.
+            if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
+                // The versioned UID is used ideally the version ID of a translated record, so this takes precedence over the localized UID
+                if ($value->_hasProperty('_versionedUid') && $value->_getProperty('_versionedUid') > 0 && $value->_getProperty('_versionedUid') !== $value->getUid()) {
+                    $value = (int)$value->_getProperty('_versionedUid');
+                }
+            }
+            $constraint = $query->equals($columnMap->getParentKeyFieldName(), $value);
             if ($columnMap->getParentTableFieldName() !== null) {
                 $constraint = $query->logicalAnd(
                     $constraint,
-                    $query->equals($columnMap->getParentTableFieldName(), $this->getDataMap(get_class($parentObject))->getTableName())
+                    $query->equals($columnMap->getParentTableFieldName(), $dataMap->getTableName())
                 );
             }
         } else {
@@ -463,6 +501,58 @@ class DataMapper
         return $constraint;
     }
 
+    /**
+     * This resolves relations via RelationHandler and returns their UIDs respectively, and works for MM/ForeignField/CSV in IRRE + Select + Group.
+     *
+     * Note: This only happens for resolving properties for models. When limiting a parentQuery, the Typo3DbQueryParser is taking care of it.
+     *
+     * By using the RelationHandler, the localized, deleted and moved records turn out to be properly resolved
+     * without having to build intermediate queries.
+     *
+     * This is currently only used in workspaces' context, as it is 1 additional DB query needed.
+     *
+     * @param DataMap $dataMap
+     * @param ColumnMap $columnMap
+     * @param DomainObjectInterface $parentObject
+     * @param string $fieldValue
+     * @param int $workspaceId
+     * @return array|false|mixed
+     */
+    protected function resolveRelationValuesOfField(DataMap $dataMap, ColumnMap $columnMap, DomainObjectInterface $parentObject, $fieldValue, int $workspaceId)
+    {
+        $parentId = $parentObject->getUid();
+        // versionedUid in a multi-language setup is the overlaid versioned AND translated ID
+        if ($parentObject->_hasProperty('_versionedUid') && $parentObject->_getProperty('_versionedUid') > 0 && $parentObject->_getProperty('_versionedUid') !== $parentId) {
+            $parentId = $parentObject->_getProperty('_versionedUid');
+        } elseif ($parentObject->_hasProperty('_languageUid') && $parentObject->_getProperty('_languageUid') > 0) {
+            $parentId = $parentObject->_getProperty('_localizedUid');
+        }
+        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
+        $relationHandler->setWorkspaceId($workspaceId);
+        $relationHandler->setUseLiveReferenceIds(true);
+        $relationHandler->setUseLiveParentIds(true);
+        $tableName = $dataMap->getTableName();
+        $fieldName = $columnMap->getColumnName();
+        $fieldConfiguration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? null;
+        if (!is_array($fieldConfiguration)) {
+            return [];
+        }
+        $relationHandler->start(
+            $fieldValue,
+            $fieldConfiguration['allowed'] ?? $fieldConfiguration['foreign_table'] ?? '',
+            $fieldConfiguration['MM'] ?? '',
+            $parentId,
+            $tableName,
+            $fieldConfiguration
+        );
+        $relationHandler->processDeletePlaceholder();
+        $relatedUids = [];
+        if (!empty($relationHandler->tableArray)) {
+            $relatedUids = reset($relationHandler->tableArray);
+        }
+        return $relatedUids;
+    }
+
     /**
      * Builds and returns the source to build a join for a m:n relation.
      *
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
index 81f6e207ad4c..aa9651e73420 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
@@ -193,8 +193,7 @@ class WorkspaceTest extends FunctionalTestCase
         /** @var Blog $blog */
         $blog = $query->execute()->getFirst();
         self::assertEquals('WorkspaceOverlay Blog1', $blog->getTitle());
-        // @todo: this is wrong and will be fixed with https://review.typo3.org/c/Packages/TYPO3.CMS/+/68913
-        self::assertCount(3, $blog->getCategories());
+        self::assertCount(2, $blog->getCategories());
     }
 
     /**
-- 
GitLab