diff --git a/Neos.ContentRepository/Classes/Domain/Model/Node.php b/Neos.ContentRepository/Classes/Domain/Model/Node.php index 551a73c273d..d5d64d6a0ac 100644 --- a/Neos.ContentRepository/Classes/Domain/Model/Node.php +++ b/Neos.ContentRepository/Classes/Domain/Model/Node.php @@ -1663,7 +1663,40 @@ protected function createRecursiveCopy(NodeInterface $referenceNode, string $nod $referenceNodeDimensionsHash = Utility::sortDimensionValueArrayAndReturnDimensionsHash($referenceNodeDimensions); $thisDimensions = $this->getDimensions(); $thisNodeDimensionsHash = Utility::sortDimensionValueArrayAndReturnDimensionsHash($thisDimensions); - if ($detachedCopy === false && $referenceNodeDimensionsHash !== $thisNodeDimensionsHash && $referenceNode->getContext()->getNodeByIdentifier($this->getIdentifier()) === null) { + + // We are only allowed to re-use the node's identifier if the copy-target's context (from $referenceNode) + // does NOT contain this identifier. + // We need to check this also taking removed and invisible nodes into account. + // + // Without changing the context, removedContentShown is typically FALSE, leading to the FOLLOWING BUG: + // + // PREREQUISITES: + // - a language dimension with two values, without fallbacks ("de" and "en") + // - create a page in DE with content nodes "text1" and "text2" + // - translate this page to EN and let it copy all content. "text1" also exists on EN now, and has the same identifier as in DE. + // - publish everything. + // + // REPRODUCING THE BUG: (comment out the removedContentShown line below) + // - select "text1" in "DE" and copy it + // - switch to EN + // - REMOVE the node "text1" in EN + // - PASTE the node from the clipboard AFTER text2 (in EN). + // - (this triggers the code we have here.) + // + // EXPECTED BEHAVIOR + // - the pasted node is shown + // + // ACTUAL BEHAVIOR + // - the pasted node is not shown, but is still in the database. + // - it can happen that the node *is* shown, if it is inserted above the removed node. Still, we have an invariant violation nevertheless. + // - this can also trigger problems when **publishing** the not-rendered-anymore-node (UniqueConstraint errors in the database) - this is + // how we actually found the error. + $contextPropertiesWithAllNodesShown = $referenceNode->getContext()->getProperties(); + $contextPropertiesWithAllNodesShown['invisibleContentShown'] = true; + $contextPropertiesWithAllNodesShown['removedContentShown'] = true; + $contextPropertiesWithAllNodesShown['inaccessibleContentShown'] = true; + $referenceNodeContextWithAllNodesShown = $this->contextFactory->create($contextPropertiesWithAllNodesShown); + if ($detachedCopy === false && $referenceNodeDimensionsHash !== $thisNodeDimensionsHash && $referenceNodeContextWithAllNodesShown->getNodeByIdentifier($this->getIdentifier()) === null) { // If the target dimensions are different than this one, and there is no node shadowing this one in the target dimension yet, we use the same // node identifier, effectively creating a new node variant. $identifier = $this->getIdentifier(); diff --git a/Neos.ContentRepository/Tests/Functional/Domain/CopyNodeAcrossDimensionsBug3265Test.php b/Neos.ContentRepository/Tests/Functional/Domain/CopyNodeAcrossDimensionsBug3265Test.php new file mode 100644 index 00000000000..c3946eee463 --- /dev/null +++ b/Neos.ContentRepository/Tests/Functional/Domain/CopyNodeAcrossDimensionsBug3265Test.php @@ -0,0 +1,223 @@ +persistAndResetStateCompletely(); + } + + /** + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + $this->inject($this->contextFactory, 'contextInstances', []); + $configuredDimensions = $this->objectManager->get(ConfigurationManager::class)->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.ContentRepository.contentDimensions'); + $this->contentDimensionRepository->setDimensionsConfiguration($configuredDimensions); + } + + /** + * @test + */ + public function testForBug3265() + { + // FIXTURE SETUP + // node1 in DE and EN (live) + // node2 in DE and EN (live) + $this->liveContextDe->getRootNode()->createNode('node1', null, 'node1-identifier'); + $this->liveContextDe->getRootNode()->createNode('node2', null, 'node2-identifier'); + $this->liveContextEn->getRootNode()->createNode('node1', null, 'node1-identifier'); + $this->liveContextEn->getRootNode()->createNode('node2', null, 'node2-identifier'); + $this->persistAndResetStateCompletely(); + + // REPRODUCING THE PROBLEM + // 1) we remove the node1 in DE (in user workspace) + $this->userWorkspaceContextDe->getRootNode()->getNode('node1')->remove(); + $this->persistAndResetStateCompletely(); + + // 2) we copy the node1 from EN to DE after node2 (in user workspace) + $enNode1 = $this->userWorkspaceContextEn->getRootNode()->getNode('node1'); + $deNode2 = $this->userWorkspaceContextDe->getRootNode()->getNode('node2'); + $this->assertNotNull($enNode1, 'enNode1 must not be null'); + $this->assertNotNull($deNode2, 'deNode2 must not be null'); + $enNode1->copyAfter($deNode2, 'node-1-pasted'); + $this->persistAndResetStateCompletely(); + + // EXPECTATION / ASSERTION + // the copied node must appear underneath the root + $childNodes = $this->userWorkspaceContextDe->getRootNode()->getChildNodes(); + $childNodePaths = []; + foreach ($childNodes as $node) { + $childNodePaths[] = $node->getPath(); + } + $expected = ['/node2', '/node-1-pasted']; + $this->assertEquals($expected, $childNodePaths, 'We copied node-1-pasted, but it does not appear'); + } + + protected function persistAndResetStateCompletely() + { + if ($this->nodeDataRepository !== null) { + $this->nodeDataRepository->flushNodeRegistry(); + } + /** @var NodeFactory $nodeFactory */ + $nodeFactory = $this->objectManager->get(NodeFactory::class); + $nodeFactory->reset(); + $this->contextFactory = $this->objectManager->get(ContextFactoryInterface::class); + $this->contextFactory->reset(); + $this->persistenceManager->persistAll(); + $this->persistenceManager->clearState(); + $this->nodeDataRepository = null; + $this->rootNode = null; + + $this->nodeDataRepository = new NodeDataRepository(); + + if ($this->liveWorkspace === null) { + $this->liveWorkspace = new Workspace('live'); + $this->objectManager->get(WorkspaceRepository::class); + $this->workspaceRepository = $this->objectManager->get(WorkspaceRepository::class); + $this->workspaceRepository->add($this->liveWorkspace); + $this->workspaceRepository->add(new Workspace('user-admin', $this->liveWorkspace)); + $this->persistenceManager->persistAll(); + } + + + $this->liveContextDe = $this->contextFactory->create([ + 'workspaceName' => 'live', + 'dimensions' => [ + 'language' => ['de'] + ], + 'targetDimensions' => [ + 'language' => 'de' + ] + ]); + $this->liveContextEn = $this->contextFactory->create([ + 'workspaceName' => 'live', + 'dimensions' => [ + 'language' => ['en'] + ], + 'targetDimensions' => [ + 'language' => 'en' + ] + ]); + $this->userWorkspaceContextDe = $this->contextFactory->create([ + 'workspaceName' => 'user-admin', + 'dimensions' => [ + 'language' => ['de'] + ], + 'targetDimensions' => [ + 'language' => 'de' + ] + ]); + $this->userWorkspaceContextEn = $this->contextFactory->create([ + 'workspaceName' => 'user-admin', + 'dimensions' => [ + 'language' => ['en'] + ], + 'targetDimensions' => [ + 'language' => 'en' + ] + ]); + $this->nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); + $this->contentDimensionRepository = $this->objectManager->get(ContentDimensionRepository::class); + + $this->contentDimensionRepository->setDimensionsConfiguration([ + 'language' => [ + 'default' => 'en', + 'presets' => [ + 'enPreset' => [ + 'values' => ['en'] + ], + 'dePreset' => [ + 'values' => ['de'] + ] + ] + ] + ]); + } +} diff --git a/Neos.Media.Browser/Resources/Private/Templates/Asset/Index.html b/Neos.Media.Browser/Resources/Private/Templates/Asset/Index.html index 2da276470c0..e4d06ff0e35 100644 --- a/Neos.Media.Browser/Resources/Private/Templates/Asset/Index.html +++ b/Neos.Media.Browser/Resources/Private/Templates/Asset/Index.html @@ -319,10 +319,11 @@