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