/** * @test */ public function aWorkspaceCanBeBasedOnAnotherWorkspace() { $baseWorkspace = new Workspace('BaseWorkspace'); $workspace = new Workspace('MyWorkspace', $baseWorkspace); $this->assertSame('MyWorkspace', $workspace->getName()); $this->assertSame($baseWorkspace, $workspace->getBaseWorkspace()); }
/** * @test */ public function getUnpublishedNodesDoesNotReturnInvalidNodes() { $mockContext = $this->getMockBuilder('TYPO3\\TYPO3CR\\Domain\\Service\\Context')->disableOriginalConstructor()->getMock(); $expectedContextProperties = array('workspaceName' => $this->mockWorkspace->getName(), 'inaccessibleContentShown' => true, 'invisibleContentShown' => true, 'removedContentShown' => true, 'currentSite' => $this->mockSite, 'dimensions' => array()); $this->mockContextFactory->expects($this->any())->method('create')->with($expectedContextProperties)->will($this->returnValue($mockContext)); $mockNodeData1 = $this->getMockBuilder('TYPO3\\TYPO3CR\\Domain\\Model\\NodeData')->disableOriginalConstructor()->getMock(); $mockNodeData2 = $this->getMockBuilder('TYPO3\\TYPO3CR\\Domain\\Model\\NodeData')->disableOriginalConstructor()->getMock(); $mockNodeData1->expects($this->any())->method('getDimensionValues')->will($this->returnValue(array())); $mockNodeData2->expects($this->any())->method('getDimensionValues')->will($this->returnValue(array())); $mockNode1 = $this->getMockBuilder('TYPO3\\TYPO3CR\\Domain\\Model\\NodeInterface')->getMock(); $mockNode1->expects($this->any())->method('getNodeData')->will($this->returnValue($mockNodeData1)); $mockNode1->expects($this->any())->method('getPath')->will($this->returnValue('/node1')); $this->mockNodeFactory->expects($this->at(0))->method('createFromNodeData')->with($mockNodeData1, $mockContext)->will($this->returnValue($mockNode1)); $this->mockNodeFactory->expects($this->at(1))->method('createFromNodeData')->with($mockNodeData2, $mockContext)->will($this->returnValue(null)); $this->mockNodeDataRepository->expects($this->atLeastOnce())->method('findByWorkspace')->with($this->mockWorkspace)->will($this->returnValue(array($mockNodeData1, $mockNodeData2))); $actualResult = $this->publishingService->getUnpublishedNodes($this->mockWorkspace); $this->assertSame($actualResult, array($mockNode1)); }
/** * Checks if this instance matches the given workspace and dimensions. * * @param Workspace $workspace * @param array $dimensions * @return boolean */ public function matchesWorkspaceAndDimensions($workspace, array $dimensions = null) { if ($this->workspace->getName() !== $workspace->getName()) { return false; } if ($dimensions !== null) { $nodeDimensionValues = $this->dimensionValues; foreach ($dimensions as $dimensionName => $dimensionValues) { if (!isset($nodeDimensionValues[$dimensionName]) || array_intersect($nodeDimensionValues[$dimensionName], $dimensionValues) === array()) { return false; } } } return true; }
/** * Creates a new content context based on the given workspace and the NodeData object. * * @param Workspace $workspace Workspace for the new context * @param array $dimensionValues The dimension values for the new context * @param array $contextProperties Additional pre-defined context properties * @return Context */ protected function createContext(Workspace $workspace, array $dimensionValues, array $contextProperties = array()) { $contextProperties += array('workspaceName' => $workspace->getName(), 'inaccessibleContentShown' => TRUE, 'invisibleContentShown' => TRUE, 'removedContentShown' => TRUE, 'dimensions' => $dimensionValues); return $this->contextFactory->create($contextProperties); }
/** * Sets the workspace of this node. * * This method is only for internal use by the content repository. Changing * the workspace of a node manually may lead to unexpected behavior. * * @param Workspace $workspace * @return void */ public function setWorkspace(Workspace $workspace) { if (!$this->isNodeDataMatchingContext()) { $this->materializeNodeData(); } if ($this->getWorkspace()->getName() === $workspace->getName()) { return; } $this->nodeData->setWorkspace($workspace); $this->context->getFirstLevelNodeCache()->flush(); $this->emitNodeUpdated($this); }
/** * Checks if this instance matches the given workspace and dimensions. * * @param Workspace $workspace * @param array $dimensions * @return boolean */ public function matchesWorkspaceAndDimensions($workspace, array $dimensions = NULL) { if ($this->workspace->getName() !== $workspace->getName()) { return FALSE; } if ($dimensions !== NULL) { $nodeDimensionValues = $this->getDimensionValues(); foreach ($dimensions as $dimensionName => $dimensionValues) { if (!isset($nodeDimensionValues[$dimensionName]) || array_intersect($nodeDimensionValues[$dimensionName], $dimensionValues) === array()) { return FALSE; } } } return TRUE; }
/** * * * @param NodeInterface $node * @param Workspace $targetWorkspace * @return void */ public function afterNodePublishing(NodeInterface $node, Workspace $targetWorkspace) { if (!$this->eventEmittingService->isEnabled()) { return; } $documentNode = NodeEvent::getClosestAggregateNode($node); if ($documentNode === null) { return; } $this->scheduledNodeEventUpdates[$documentNode->getContextPath()] = array('workspaceName' => $node->getContext()->getWorkspaceName(), 'nestedNodeIdentifiersWhichArePublished' => array(), 'targetWorkspace' => $targetWorkspace->getName(), 'documentNode' => $documentNode); $this->scheduledNodeEventUpdates[$documentNode->getContextPath()]['nestedNodeIdentifiersWhichArePublished'][] = $node->getIdentifier(); }
/** * Creates a new content context based on the given workspace and the NodeData object. * * @param Workspace $workspace Workspace for the new context * @param array $dimensionValues The dimension values for the new context * @param array $contextProperties Additional pre-defined context properties * @return Context */ protected function createContext(Workspace $workspace, array $dimensionValues, array $contextProperties = array()) { $presetsMatchingDimensionValues = $this->contentDimensionPresetSource->findPresetsByTargetValues($dimensionValues); $dimensions = array_map(function ($preset) { return $preset['values']; }, $presetsMatchingDimensionValues); $contextProperties += array('workspaceName' => $workspace->getName(), 'inaccessibleContentShown' => true, 'invisibleContentShown' => true, 'removedContentShown' => true, 'dimensions' => $dimensions); return $this->contextFactory->create($contextProperties); }
/** * Delete a workspace * * @param Workspace $workspace A workspace to delete * @return void */ public function deleteAction(Workspace $workspace) { if (substr($workspace->getName(), 0, 5) === 'user-') { $this->redirect('index'); } $dependentWorkspaces = $this->workspaceRepository->findByBaseWorkspace($workspace); if (count($dependentWorkspaces) > 0) { $dependentWorkspaceTitles = []; /** @var Workspace $dependentWorkspace */ foreach ($dependentWorkspaces as $dependentWorkspace) { $dependentWorkspaceTitles[] = $dependentWorkspace->getTitle(); } $message = $this->translator->translateById('workspaces.workspaceCannotBeDeletedBecauseOfDependencies', [$workspace->getTitle(), implode(', ', $dependentWorkspaceTitles)], null, null, 'Modules', 'TYPO3.Neos'); $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); $this->redirect('index'); } $nodesCount = 0; try { $nodesCount = $this->publishingService->getUnpublishedNodesCount($workspace); } catch (\Exception $exception) { $message = $this->translator->translateById('workspaces.notDeletedErrorWhileFetchingUnpublishedNodes', [$workspace->getTitle()], null, null, 'Modules', 'TYPO3.Neos'); $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); $this->redirect('index'); } if ($nodesCount > 0) { $message = $this->translator->translateById('workspaces.workspaceCannotBeDeletedBecauseOfUnpublishedNodes', [$workspace->getTitle(), $nodesCount], $nodesCount, null, 'Modules', 'TYPO3.Neos'); $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); $this->redirect('index'); } $this->workspaceRepository->remove($workspace); $this->addFlashMessage($message = $this->translator->translateById('workspaces.workspaceHasBeenRemoved', [$workspace->getTitle()], null, null, 'Modules', 'TYPO3.Neos')); $this->redirect('index'); }
/** * Update a workspace * * @param Workspace $workspace A workspace to update * @return void */ public function updateAction(Workspace $workspace) { if ($workspace->getTitle() === '') { $workspace->setTitle($workspace->getName()); } $this->workspaceRepository->update($workspace); $this->addFlashMessage($this->translator->translateById('workspaces.workspaceHasBeenUpdated', [$workspace->getTitle()], null, null, 'Modules', 'TYPO3.Neos')); $this->redirect('index'); }
/** * Checks if the current user may read the given workspace according to one the roles of the user's accounts * * In future versions, this logic may be implemented in Neos in a more generic way (for example, by means of an * ACL object), but for now, this method exists in order to at least centralize and encapsulate the required logic. * * @param Workspace $workspace The workspace * @return boolean */ public function currentUserCanReadWorkspace(Workspace $workspace) { if ($workspace->getName() === 'live') { return true; } if ($workspace->getOwner() === $this->getCurrentUser() || $workspace->getOwner() === null) { return true; } return false; }
/** * Publishes the whole workspace * * @param Workspace $workspace * @return void */ public function publishWorkspaceAction(Workspace $workspace) { $liveWorkspace = $this->workspaceRepository->findOneByName('live'); $workspace->publish($liveWorkspace); $this->addFlashMessage('Changes in workspace "%s" have been published', 'Changes published', Message::SEVERITY_OK, array($workspace->getName()), 1412420808); $this->redirect('index'); }
/** * When moving nodes which are inside live workspace to a personal workspace *across levels* (i.e. with different * parent node before and after), the system returned *both* the "new" node from the personal workspace (correct!), * and the "shined-through" version of the node from the "live" workspace (WRONG!). * * For all nodes not being in our base workspace, we need to check whether it is overlaid by a node in our base workspace * with the same identifier. If that's the case, we do not show the node. * * This is a bugfix for #48214. * * @param array $foundNodes * @param Workspace $baseWorkspace * @param array $dimensions * @return array */ protected function filterNodesOverlaidInBaseWorkspace(array $foundNodes, Workspace $baseWorkspace, array $dimensions = NULL) { $identifiersOfNodesNotInBaseWorkspace = array(); /** @var $foundNode NodeData */ foreach ($foundNodes as $i => $foundNode) { if ($foundNode->getWorkspace()->getName() !== $baseWorkspace->getName()) { $identifiersOfNodesNotInBaseWorkspace[$foundNode->getIdentifier()] = $i; } } if (count($identifiersOfNodesNotInBaseWorkspace) === 0) { return $foundNodes; } /** @var \Doctrine\ORM\QueryBuilder $queryBuilder */ $queryBuilder = $this->entityManager->createQueryBuilder(); $queryBuilder->select('n.identifier')->distinct()->from('TYPO3\\TYPO3CR\\Domain\\Model\\NodeData', 'n')->where('n.workspace = :baseWorkspace')->andWhere('n.identifier IN (:identifierList)')->setParameter('baseWorkspace', $baseWorkspace)->setParameter('identifierList', array_keys($identifiersOfNodesNotInBaseWorkspace)); if ($dimensions !== NULL) { $this->addDimensionJoinConstraintsToQueryBuilder($queryBuilder, $dimensions); } $results = $queryBuilder->getQuery()->getArrayResult(); foreach ($results as $result) { $nodeIdentifierOfNodeInBaseWorkspace = $result['identifier']; $indexOfNodeNotInBaseWorkspaceWhichShouldBeRemoved = $identifiersOfNodesNotInBaseWorkspace[$nodeIdentifierOfNodeInBaseWorkspace]; unset($foundNodes[$indexOfNodeNotInBaseWorkspaceWhichShouldBeRemoved]); } return $foundNodes; }
/** * Checks if the current user may publish to the given workspace according to one the roles of the user's accounts * * In future versions, this logic may be implemented in Neos in a more generic way (for example, by means of an * ACL object), but for now, this method exists in order to at least centralize and encapsulate the required logic. * * @param Workspace $workspace The workspace * @return boolean */ public function currentUserCanPublishToWorkspace(Workspace $workspace) { if ($workspace->getName() === 'live') { return $this->securityContext->hasRole('TYPO3.Neos:LivePublisher'); } if ($workspace->getOwner() === $this->getCurrentUser() || $workspace->getOwner() === null) { return true; } return false; }
/** * Move this NodeData to the given path and workspace. * * Basically 4 scenarios have to be covered here, depending on: * * - Does the NodeData have to be materialized (adapted to the workspace or target dimension)? * - Does a shadow node exist on the target path? * * Because unique key constraints and Doctrine ORM don't support arbitrary removal and update combinations, * existing NodeData instances are re-used and the metadata and content is swapped around. * * @param string $path * @param Workspace $workspace * @return NodeData If a shadow node was created this is the new NodeData object after the move. */ public function move($path, $workspace) { $nodeData = $this; $originalPath = $this->path; if ($originalPath === $path) { return $this; } $targetPathShadowNodeData = $this->getExistingShadowNodeData($path, $workspace, $nodeData->getDimensionValues()); if ($this->workspace->getName() !== $workspace->getName()) { if ($targetPathShadowNodeData === null) { // If there is no shadow node at the target path we need to materialize before moving and create a shadow node. $nodeData = $this->materializeToWorkspace($workspace); $nodeData->setPath($path, false); $movedNodeData = $nodeData; } else { // The existing shadow node on the target path will be used as the moved node. We don't need to materialize into the workspace. $movedNodeData = $targetPathShadowNodeData; $movedNodeData->setAsShadowOf(null); $movedNodeData->setIdentifier($nodeData->getIdentifier()); $movedNodeData->similarize($nodeData); // A new shadow node will be created for the node data that references the recycled, existing shadow node } $movedNodeData->createShadow($originalPath); } else { $referencedShadowNode = $this->nodeDataRepository->findOneByMovedTo($nodeData); if ($targetPathShadowNodeData === null) { if ($referencedShadowNode === null) { // There is no shadow node on the original or target path, so the current node data will be turned to a shadow node and a new node data will be created for the moved node. // We cannot just create a new shadow node, since the order of Doctrine queries would cause unique key conflicts $movedNodeData = new NodeData($path, $nodeData->getWorkspace(), $nodeData->getIdentifier(), $nodeData->getDimensionValues()); $movedNodeData->similarize($nodeData); $this->addOrUpdate($movedNodeData); $shadowNodeData = $nodeData; $shadowNodeData->setAsShadowOf($movedNodeData); } else { // A shadow node that references this node data already exists, so we just move the current node data $movedNodeData = $nodeData; $movedNodeData->setPath($path, false); } } else { if ($referencedShadowNode === null) { // Turn the target path shadow node into the moved node (and adjust the identifier!) // Do not reset the movedTo property to keep tracing the original move operation - TODO: Does that make sense if the identifier changes? $movedNodeData = $targetPathShadowNodeData; // Since the shadow node at the target path does not belong to the current node, we have to adjust the identifier $movedNodeData->setRemoved(false); $movedNodeData->setIdentifier($nodeData->getIdentifier()); $movedNodeData->similarize($nodeData); // Create a shadow node from the current node that shadows the recycled node $shadowNodeData = $nodeData; $shadowNodeData->setAsShadowOf($movedNodeData); } else { // If there is already shadow node on the target path, we need to make that shadow node the actual moved node and remove the current node data (which cannot be live). // We cannot remove the shadow node and update the current node data, since the order of Doctrine queries would cause unique key conflicts. $movedNodeData = $targetPathShadowNodeData; $movedNodeData->setAsShadowOf(null); $movedNodeData->similarize($nodeData); $movedNodeData->setPath($path, false); $this->nodeDataRepository->remove($nodeData); } } } return $movedNodeData; }
/** * Collects all nodes with missing shadow nodes * * @param Workspace $workspace * @param boolean $dryRun * @param NodeType $nodeType * @return array */ protected function fixShadowNodesInWorkspace(Workspace $workspace, $dryRun, NodeType $nodeType = null) { $workspaces = array_merge([$workspace], $workspace->getBaseWorkspaces()); $fixedShadowNodes = 0; foreach ($workspaces as $workspace) { /** @var Workspace $workspace */ if ($workspace->getBaseWorkspace() === null) { continue; } /** @var QueryBuilder $queryBuilder */ $queryBuilder = $this->entityManager->createQueryBuilder(); $queryBuilder->select('n')->from(NodeData::class, 'n')->where('n.workspace = :workspace'); $queryBuilder->setParameter('workspace', $workspace->getName()); if ($nodeType !== null) { $queryBuilder->andWhere('n.nodeType = :nodeType'); $queryBuilder->setParameter('nodeType', $nodeType->getName()); } /** @var NodeData $nodeData */ foreach ($queryBuilder->getQuery()->getResult() as $nodeData) { $nodeDataSeenFromParentWorkspace = $this->nodeDataRepository->findOneByIdentifier($nodeData->getIdentifier(), $workspace->getBaseWorkspace(), $nodeData->getDimensionValues()); // This is the good case, either the node does not exist or was shadowed if ($nodeDataSeenFromParentWorkspace === null) { continue; } // Also good, the node was not moved at all. if ($nodeDataSeenFromParentWorkspace->getPath() === $nodeData->getPath()) { continue; } $nodeDataOnSamePath = $this->nodeDataRepository->findOneByPath($nodeData->getPath(), $workspace->getBaseWorkspace(), $nodeData->getDimensionValues(), null); // We cannot just put a shadow node in the path, something exists, but that should be fine. if ($nodeDataOnSamePath !== null) { continue; } if (!$dryRun) { $nodeData->createShadow($nodeDataSeenFromParentWorkspace->getPath()); } $fixedShadowNodes++; } } return $fixedShadowNodes; }
/** * Checks if the specified workspace is a base workspace of this workspace * and if not, throws an exception * * @param Workspace $targetWorkspace The publishing target workspace * @return void * @throws WorkspaceException if the specified workspace is not a base workspace of this workspace */ protected function verifyPublishingTargetWorkspace(Workspace $targetWorkspace) { $baseWorkspace = $this->baseWorkspace; while ($targetWorkspace !== $baseWorkspace) { if ($baseWorkspace === null) { throw new WorkspaceException(sprintf('The specified workspace "%s" is not a base workspace of "%s".', $targetWorkspace->getName(), $this->getName()), 1289499117); } $baseWorkspace = $baseWorkspace->getBaseWorkspace(); } }
/** * Returns the NodeData instance with the given identifier from the target workspace. * If no NodeData instance is found, NULL is returned. * * @param NodeInterface $node * @param Workspace $targetWorkspace * @return NodeData */ protected function findNodeDataInTargetWorkspace(NodeInterface $node, Workspace $targetWorkspace) { $properties = $node->getContext()->getProperties(); $properties['workspaceName'] = $targetWorkspace->getName(); $targetWorkspaceContext = $this->contextFactory->create($properties); $targetNodeInstance = $targetWorkspaceContext->getNodeByIdentifier($node->getIdentifier()); $targetNode = $targetNodeInstance !== NULL ? $targetNodeInstance->getNodeData() : NULL; return $targetNode; }