diff --git a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php index a5abce1320b..7397fbf44f4 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php +++ b/Neos.ContentRepository.Core/Classes/Feature/Common/ConstraintChecks.php @@ -164,9 +164,9 @@ protected function requireRootNodeTypeToBeUnoccupied( protected function requireTetheredDescendantNodeTypesToExist(NodeType $nodeType): void { // this getter throws if any of the child nodeTypes doesnt exist! - $childNodeTypes = $nodeType->getAutoCreatedChildNodes(); - foreach ($childNodeTypes as $childNodeType) { - $this->requireTetheredDescendantNodeTypesToExist($childNodeType); + $tetheredNodeTypes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType); + foreach ($tetheredNodeTypes as $tetheredNodeType) { + $this->requireTetheredDescendantNodeTypesToExist($tetheredNodeType); } } @@ -176,7 +176,7 @@ protected function requireTetheredDescendantNodeTypesToExist(NodeType $nodeType) */ protected function requireTetheredDescendantNodeTypesToNotBeOfTypeRoot(NodeType $nodeType): void { - foreach ($nodeType->getAutoCreatedChildNodes() as $tetheredChildNodeType) { + foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $tetheredChildNodeType) { if ($tetheredChildNodeType->isOfType(NodeTypeName::ROOT_NODE_TYPE_NAME)) { throw new NodeTypeIsOfTypeRoot( 'Node type "' . $nodeType->name->value . '" for tethered descendant is of type root.', @@ -301,12 +301,12 @@ protected function requireNodeTypeConstraintsImposedByParentToBeMet( } if ( $nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeName) - && !$parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->equals($nodeType->name) + && $parentsNodeType->hasTetheredNode($nodeName) + && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) ) { throw new NodeConstraintException( 'Node type "' . $nodeType->name->value . '" does not match configured "' - . $parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->value + . $this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->value . '" for auto created child nodes for parent type "' . $parentsNodeType->name->value . '" with name "' . $nodeName->value . '"' ); @@ -324,8 +324,8 @@ protected function areNodeTypeConstraintsImposedByParentValid( } if ( $nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeName) - && !$parentsNodeType->getTypeOfAutoCreatedChildNode($nodeName)?->name->equals($nodeType->name) + && $parentsNodeType->hasTetheredNode($nodeName) + && !$this->getNodeTypeManager()->getTypeOfTetheredNode($parentsNodeType, $nodeName)->name->equals($nodeType->name) ) { return false; } @@ -362,8 +362,8 @@ protected function areNodeTypeConstraintsImposedByGrandparentValid( ): bool { if ( $parentNodeName - && $grandParentsNodeType->hasAutoCreatedChildNode($parentNodeName) - && !$grandParentsNodeType->allowsGrandchildNodeType($parentNodeName->value, $nodeType) + && $grandParentsNodeType->hasTetheredNode($parentNodeName) + && !$this->getNodeTypeManager()->isNodeTypeAllowedAsChildToTetheredNode($grandParentsNodeType, $parentNodeName, $nodeType) ) { return false; } diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php index 2896af0cd29..fb8e7de7aa0 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeAggregateCommandHandler.php @@ -211,8 +211,8 @@ protected function checkConstraintsImposedByAncestors( } if ( $nodeAggregate->nodeName - && $parentsNodeType->hasAutoCreatedChildNode($nodeAggregate->nodeName) - && $parentsNodeType->getTypeOfAutoCreatedChildNode($nodeAggregate->nodeName)?->name + && $parentsNodeType->hasTetheredNode($nodeAggregate->nodeName) + && $this->nodeTypeManager->getTypeOfTetheredNode($parentsNodeType, $nodeAggregate->nodeName)->name !== $command->newNodeTypeName->value ) { throw new NodeConstraintException( @@ -232,9 +232,10 @@ protected function checkConstraintsImposedByAncestors( ); if ( $parentAggregate->nodeName - && $grandParentsNodeType->hasAutoCreatedChildNode($parentAggregate->nodeName) - && !$grandParentsNodeType->allowsGrandchildNodeType( - $parentAggregate->nodeName->value, + && $grandParentsNodeType->hasTetheredNode($parentAggregate->nodeName) + && !$this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode( + $grandParentsNodeType, + $parentAggregate->nodeName, $newNodeType ) ) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php index 46ef4e6a46b..8a7b8d12554 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeCreation/NodeCreation.php @@ -31,6 +31,7 @@ use Neos\ContentRepository\Core\Infrastructure\Property\PropertyConverter; use Neos\ContentRepository\Core\Infrastructure\Property\PropertyType; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; @@ -61,6 +62,8 @@ abstract protected function requireNodeTypeToBeOfTypeRoot(NodeType $nodeType): v abstract protected function getPropertyConverter(): PropertyConverter; + abstract protected function getNodeTypeManager(): NodeTypeManager; + private function handleCreateNodeAggregateWithNode( CreateNodeAggregateWithNode $command, ContentRepository $contentRepository @@ -203,6 +206,7 @@ private function handleCreateNodeAggregateWithNodeAndSerializedProperties( } $descendantNodeAggregateIds = self::populateNodeAggregateIds( $nodeType, + $this->getNodeTypeManager(), $command->tetheredDescendantNodeAggregateIds ); // Write the auto-created descendant node aggregate ids back to the command; @@ -282,7 +286,7 @@ private function handleTetheredChildNodes( ContentRepository $contentRepository, ): Events { $events = []; - foreach ($nodeType->getAutoCreatedChildNodes() as $rawNodeName => $childNodeType) { + foreach ($this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($nodeType) as $rawNodeName => $childNodeType) { assert($childNodeType instanceof NodeType); $nodeName = NodeName::fromString($rawNodeName); $childNodePath = $nodePath @@ -343,6 +347,7 @@ private function createTetheredWithNode( protected static function populateNodeAggregateIds( NodeType $nodeType, + NodeTypeManager $nodeTypeManager, ?NodeAggregateIdsByNodePaths $nodeAggregateIds, NodePath $childPath = null ): NodeAggregateIdsByNodePaths { @@ -350,7 +355,7 @@ protected static function populateNodeAggregateIds( $nodeAggregateIds = NodeAggregateIdsByNodePaths::createEmpty(); } // TODO: handle Multiple levels of autocreated child nodes - foreach ($nodeType->getAutoCreatedChildNodes() as $rawChildName => $childNodeType) { + foreach ($nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType) as $rawChildName => $childNodeType) { $childName = NodeName::fromString($rawChildName); $childPath = $childPath ? $childPath->appendPathSegment($childName) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php index f174092536a..478bab25b41 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeTypeChange/NodeTypeChange.php @@ -26,6 +26,7 @@ use Neos\ContentRepository\Core\Feature\NodeTypeChange\Command\ChangeNodeAggregateType; use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; @@ -49,6 +50,8 @@ */ trait NodeTypeChange { + abstract protected function getNodeTypeManager(): NodeTypeManager; + abstract protected function requireProjectedNodeAggregate( ContentStreamId $contentStreamId, NodeAggregateId $nodeAggregateId, @@ -89,6 +92,7 @@ abstract protected function areNodeTypeConstraintsImposedByGrandparentValid( abstract protected static function populateNodeAggregateIds( NodeType $nodeType, + NodeTypeManager $nodeTypeManager, NodeAggregateIdsByNodePaths $nodeAggregateIds, NodePath $childPath = null ): NodeAggregateIdsByNodePaths; @@ -158,6 +162,7 @@ private function handleChangeNodeAggregateType( **************/ $descendantNodeAggregateIds = static::populateNodeAggregateIds( $newNodeType, + $this->getNodeTypeManager(), $command->tetheredDescendantNodeAggregateIds ); // Write the auto-created descendant node aggregate ids back to the command; @@ -190,7 +195,7 @@ private function handleChangeNodeAggregateType( } // new tethered child nodes - $expectedTetheredNodes = $newNodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); foreach ($nodeAggregate->getNodes() as $node) { assert($node instanceof Node); foreach ($expectedTetheredNodes as $serializedTetheredNodeName => $expectedTetheredNodeType) { @@ -371,7 +376,7 @@ private function deleteObsoleteTetheredNodesWhenChangingNodeType( NodeType $newNodeType, ContentRepository $contentRepository ): Events { - $expectedTetheredNodes = $newNodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->getNodeTypeManager()->getTetheredNodesConfigurationForNodeType($newNodeType); $events = []; // find disallowed tethered nodes diff --git a/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php b/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php new file mode 100644 index 00000000000..91058e9b8b8 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/NodeType/ConstraintCheck.php @@ -0,0 +1,137 @@ + $constraints + */ + public function __construct( + private readonly array $constraints + ) { + } + + public function isNodeTypeAllowed(NodeType $nodeType): bool + { + $directConstraintsResult = $this->isNodeTypeAllowedByDirectConstraints($nodeType); + if ($directConstraintsResult !== null) { + return $directConstraintsResult; + } + + $inheritanceConstraintsResult = $this->isNodeTypeAllowedByInheritanceConstraints($nodeType); + if ($inheritanceConstraintsResult !== null) { + return $inheritanceConstraintsResult; + } + + if (isset($this->constraints['*'])) { + return (bool)$this->constraints['*']; + } + + return false; + } + + /** + * @return boolean|null true if the passed $nodeType is allowed by the $constraints, null if couldn't be decided + */ + protected function isNodeTypeAllowedByDirectConstraints(NodeType $nodeType): ?bool + { + if ($this->constraints === []) { + return true; + } + + if ( + array_key_exists($nodeType->name->value, $this->constraints) + && $this->constraints[$nodeType->name->value] === true + ) { + return true; + } + + if ( + array_key_exists($nodeType->name->value, $this->constraints) + && $this->constraints[$nodeType->name->value] === false + ) { + return false; + } + + return null; + } + + /** + * This method loops over the constraints and finds node types that the given node type inherits from. For all + * matched super types, their super types are traversed to find the closest super node with a constraint which + * is used to evaluated if the node type is allowed. It finds the closest results for true and false, and uses + * the distance to choose which one wins (lowest). If no result is found the node type is allowed. + * + * @return ?boolean (null if no constraint matched) + */ + protected function isNodeTypeAllowedByInheritanceConstraints(NodeType $nodeType): ?bool + { + $constraintDistanceForTrue = null; + $constraintDistanceForFalse = null; + foreach ($this->constraints as $superType => $constraint) { + if ($nodeType->isOfType($superType)) { + $distance = $this->traverseSuperTypes($nodeType, $superType, 0); + + if ( + $constraint === true + && ($constraintDistanceForTrue === null || $constraintDistanceForTrue > $distance) + ) { + $constraintDistanceForTrue = $distance; + } + if ( + $constraint === false + && ($constraintDistanceForFalse === null || $constraintDistanceForFalse > $distance) + ) { + $constraintDistanceForFalse = $distance; + } + } + } + + if ($constraintDistanceForTrue !== null && $constraintDistanceForFalse !== null) { + return $constraintDistanceForTrue < $constraintDistanceForFalse; + } + + if ($constraintDistanceForFalse !== null) { + return false; + } + + if ($constraintDistanceForTrue !== null) { + return true; + } + + return null; + } + + /** + * This method traverses the given node type to find the first super type that matches the constraint node type. + * In case the hierarchy has more than one way of finding a path to the node type it's not taken into account, + * since the first matched is returned. This is accepted on purpose for performance reasons and due to the fact + * that such hierarchies should be avoided. + * + * Returns null if no NodeType matched + */ + protected function traverseSuperTypes( + NodeType $currentNodeType, + string $constraintNodeTypeName, + int $distance + ): ?int { + if ($currentNodeType->name->value === $constraintNodeTypeName) { + return $distance; + } + + $distance++; + foreach ($currentNodeType->getDeclaredSuperTypes() as $superType) { + $result = $this->traverseSuperTypes($superType, $constraintNodeTypeName, $distance); + if ($result !== null) { + return $result; + } + } + + return null; + } +} diff --git a/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php b/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php new file mode 100644 index 00000000000..00f6c4fd3e1 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/NodeType/Exception/TetheredNodeNotConfigured.php @@ -0,0 +1,10 @@ + $declaredSuperTypes Parent types of this node type * @param array $configuration the configuration for this node type which is defined in the schema * @throws \InvalidArgumentException + * + * @internal */ public function __construct( NodeTypeName $name, array $declaredSuperTypes, array $configuration, - private readonly NodeTypeManager $nodeTypeManager, private readonly NodeLabelGeneratorFactoryInterface $nodeLabelGeneratorFactory ) { $this->name = $name; @@ -297,6 +291,7 @@ public function isOfType(string|NodeTypeName $nodeTypeName): bool return true; } } + return false; } @@ -322,6 +317,7 @@ public function getLocalConfiguration(): array public function getFullConfiguration(): array { $this->initialize(); + return $this->fullConfiguration; } @@ -345,6 +341,7 @@ public function hasConfiguration(string $configurationPath): bool public function getConfiguration(string $configurationPath): mixed { $this->initialize(); + return ObjectAccess::getPropertyPath($this->fullConfiguration, $configurationPath); } @@ -439,234 +436,50 @@ public function getDefaultValuesForProperties(): array } /** - * Return an array with child nodes which should be automatically created - * - * @return array the key of this array is the name of the child, and the value its NodeType. - * @api + * @return bool true if $nodeName is an autocreated child node, false otherwise */ - public function getAutoCreatedChildNodes(): array + public function hasTetheredNode(NodeName $nodeName): bool { $this->initialize(); - if (!isset($this->fullConfiguration['childNodes'])) { - return []; - } - - $autoCreatedChildNodes = []; - foreach ($this->fullConfiguration['childNodes'] as $childNodeName => $childNodeConfiguration) { - if (isset($childNodeConfiguration['type'])) { - $autoCreatedChildNodes[NodeName::transliterateFromString($childNodeName)->value] - = $this->nodeTypeManager->getNodeType($childNodeConfiguration['type']); + foreach ($this->fullConfiguration['childNodes'] ?? [] as $rawChildNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + if (NodeName::transliterateFromString($rawChildNodeName)->equals($nodeName)) { + return true; + } } } - - return $autoCreatedChildNodes; + return false; } /** - * @return bool true if $nodeName is an autocreated child node, false otherwise + * @throws TetheredNodeNotConfigured if the requested tethred node is not configured. Check via {@see NodeType::hasTetheredNode()}. */ - public function hasAutoCreatedChildNode(NodeName $nodeName): bool + public function getNodeTypeNameOfTetheredNode(NodeName $nodeName): NodeTypeName { $this->initialize(); - return isset($this->fullConfiguration['childNodes'][$nodeName->value]); - } - - /** - * @throws NodeTypeNotFoundException - */ - public function getTypeOfAutoCreatedChildNode(NodeName $nodeName): ?NodeType - { - return isset($this->fullConfiguration['childNodes'][$nodeName->value]['type']) - ? $this->nodeTypeManager->getNodeType($this->fullConfiguration['childNodes'][$nodeName->value]['type']) - : null; + foreach ($this->fullConfiguration['childNodes'] ?? [] as $rawChildNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + if (NodeName::transliterateFromString($rawChildNodeName)->equals($nodeName)) { + return NodeTypeName::fromString($configurationForChildNode['type']); + } + } + } + throw new TetheredNodeNotConfigured(sprintf('The child node "%s" is not configured for node type "%s"', $nodeName->value, $this->name->value), 1694786811); } - /** * Checks if the given NodeType is acceptable as sub-node with the configured constraints, * not taking constraints of auto-created nodes into account. Thus, this method only returns * the correct result if called on NON-AUTO-CREATED nodes! * - * Otherwise, allowsGrandchildNodeType() needs to be called on the *parent node type*. + * Otherwise, isNodeTypeAllowedAsChildToTetheredNode() needs to be called on the *parent node type*. * * @return boolean true if the $nodeType is allowed as child node, false otherwise. */ public function allowsChildNodeType(NodeType $nodeType): bool { $constraints = $this->getConfiguration('constraints.nodeTypes') ?: []; - - return $this->isNodeTypeAllowedByConstraints($nodeType, $constraints); - } - - /** - * Checks if the given $nodeType is allowed as a childNode of the given $childNodeName - * (which must be auto-created in $this NodeType). - * - * Only allowed to be called if $childNodeName is auto-created. - * - * @param string $childNodeName The name of a configured childNode of this NodeType - * @param NodeType $nodeType The NodeType to check constraints for. - * @return bool true if the $nodeType is allowed as grandchild node, false otherwise. - * @throws \InvalidArgumentException If the given $childNodeName is not configured to be auto-created in $this. - */ - public function allowsGrandchildNodeType(string $childNodeName, NodeType $nodeType): bool - { - $autoCreatedChildNodes = $this->getAutoCreatedChildNodes(); - if (!isset($autoCreatedChildNodes[$childNodeName])) { - throw new \InvalidArgumentException( - 'The method "allowsGrandchildNodeType" can only be used on auto-created childNodes, ' - . 'given $childNodeName "' . $childNodeName . '" is not auto-created.', - 1403858395 - ); - } - $constraints = $autoCreatedChildNodes[$childNodeName]->getConfiguration('constraints.nodeTypes') ?: []; - - $childNodeConfiguration = []; - foreach ($this->getConfiguration('childNodes') as $name => $configuration) { - $childNodeConfiguration[NodeName::transliterateFromString($name)->value] = $configuration; - } - $childNodeConstraintConfiguration = ObjectAccess::getPropertyPath( - $childNodeConfiguration, - $childNodeName . '.constraints.nodeTypes' - ) ?: []; - - $constraints = Arrays::arrayMergeRecursiveOverrule($constraints, $childNodeConstraintConfiguration); - - return $this->isNodeTypeAllowedByConstraints($nodeType, $constraints); - } - - /** - * Internal method to check whether the passed-in $nodeType is allowed by the $constraints array. - * - * $constraints is an associative array where the key is the Node Type Name. If the value is "true", - * the node type is explicitly allowed. If the value is "false", the node type is explicitly denied. - * If nothing is specified, the fallback "*" is used. If that one is also not specified, we DENY by - * default. - * - * Super types of the given node types are also checked, so if a super type is constrained - * it will also take affect on the inherited node types. The closest constrained super type match is used. - * - * @param array $constraints - */ - protected function isNodeTypeAllowedByConstraints(NodeType $nodeType, array $constraints): bool - { - $directConstraintsResult = $this->isNodeTypeAllowedByDirectConstraints($nodeType, $constraints); - if ($directConstraintsResult !== null) { - return $directConstraintsResult; - } - - $inheritanceConstraintsResult = $this->isNodeTypeAllowedByInheritanceConstraints($nodeType, $constraints); - if ($inheritanceConstraintsResult !== null) { - return $inheritanceConstraintsResult; - } - - if (isset($constraints['*'])) { - return (bool)$constraints['*']; - } - - return false; - } - - /** - * @param array $constraints - * @return boolean true if the passed $nodeType is allowed by the $constraints - */ - protected function isNodeTypeAllowedByDirectConstraints(NodeType $nodeType, array $constraints): ?bool - { - if ($constraints === []) { - return true; - } - - if ( - array_key_exists($nodeType->name->value, $constraints) - && $constraints[$nodeType->name->value] === true - ) { - return true; - } - - if ( - array_key_exists($nodeType->name->value, $constraints) - && $constraints[$nodeType->name->value] === false - ) { - return false; - } - - return null; - } - - /** - * This method loops over the constraints and finds node types that the given node type inherits from. For all - * matched super types, their super types are traversed to find the closest super node with a constraint which - * is used to evaluated if the node type is allowed. It finds the closest results for true and false, and uses - * the distance to choose which one wins (lowest). If no result is found the node type is allowed. - * - * @param array $constraints - * @return ?boolean (null if no constraint matched) - */ - protected function isNodeTypeAllowedByInheritanceConstraints(NodeType $nodeType, array $constraints): ?bool - { - $constraintDistanceForTrue = null; - $constraintDistanceForFalse = null; - foreach ($constraints as $superType => $constraint) { - if ($nodeType->isOfType($superType)) { - $distance = $this->traverseSuperTypes($nodeType, $superType, 0); - - if ( - $constraint === true - && ($constraintDistanceForTrue === null || $constraintDistanceForTrue > $distance) - ) { - $constraintDistanceForTrue = $distance; - } - if ( - $constraint === false - && ($constraintDistanceForFalse === null || $constraintDistanceForFalse > $distance) - ) { - $constraintDistanceForFalse = $distance; - } - } - } - - if ($constraintDistanceForTrue !== null && $constraintDistanceForFalse !== null) { - return $constraintDistanceForTrue < $constraintDistanceForFalse; - } - - if ($constraintDistanceForFalse !== null) { - return false; - } - - if ($constraintDistanceForTrue !== null) { - return true; - } - - return null; - } - - /** - * This method traverses the given node type to find the first super type that matches the constraint node type. - * In case the hierarchy has more than one way of finding a path to the node type it's not taken into account, - * since the first matched is returned. This is accepted on purpose for performance reasons and due to the fact - * that such hierarchies should be avoided. - * - * Returns null if no NodeType matched - */ - protected function traverseSuperTypes( - NodeType $currentNodeType, - string $constraintNodeTypeName, - int $distance - ): ?int { - if ($currentNodeType->name->value === $constraintNodeTypeName) { - return $distance; - } - - $distance++; - foreach ($currentNodeType->getDeclaredSuperTypes() as $superType) { - $result = $this->traverseSuperTypes($superType, $constraintNodeTypeName, $distance); - if ($result !== null) { - return $result; - } - } - - return null; + return (new ConstraintCheck($constraints))->isNodeTypeAllowed($nodeType); } /** diff --git a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php index 9fa0f00408b..d01f0158edc 100644 --- a/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php +++ b/Neos.ContentRepository.Core/Classes/NodeType/NodeTypeManager.php @@ -14,9 +14,13 @@ namespace Neos\ContentRepository\Core\NodeType; +use Neos\ContentRepository\Core\NodeType\Exception\TetheredNodeNotConfigured; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsFinalException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\Utility\Arrays; +use Neos\Utility\Exception\PropertyNotAccessibleException; /** * Manager for node types @@ -182,6 +186,80 @@ public function overrideNodeTypes(array $completeNodeTypeConfiguration): void } } + /** + * @param NodeType $nodeType + * @param NodeName $tetheredNodeName + * @return NodeType + *@throws TetheredNodeNotConfigured if the requested tethered node is not configured. Check via {@see NodeType::hasTetheredNode()}. + */ + public function getTypeOfTetheredNode(NodeType $nodeType, NodeName $tetheredNodeName): NodeType + { + $nameOfTetheredNode = $nodeType->getNodeTypeNameOfTetheredNode($tetheredNodeName); + return $this->getNodeType($nameOfTetheredNode); + } + + /** + * Return an array with child nodes which should be automatically created + * + * @return array the key of this array is the name of the child, and the value its NodeType. + * @api + */ + public function getTetheredNodesConfigurationForNodeType(NodeType $nodeType): array + { + $childNodeConfiguration = $nodeType->getConfiguration('childNodes'); + $autoCreatedChildNodes = []; + foreach ($childNodeConfiguration ?? [] as $childNodeName => $configurationForChildNode) { + if (isset($configurationForChildNode['type'])) { + $autoCreatedChildNodes[NodeName::transliterateFromString($childNodeName)->value] = $this->getNodeType($configurationForChildNode['type']); + } + } + return $autoCreatedChildNodes; + } + + /** + * Checks if the given $nodeType is allowed as a childNode of the given $tetheredNodeName + * (which must be tethered in $parentNodeType). + * + * Only allowed to be called if $tetheredNodeName is actually tethered. + * + * @param NodeType $parentNodeType The NodeType to check constraints based on. + * @param NodeName $tetheredNodeName The name of a configured tethered node of this NodeType + * @param NodeType $nodeType The NodeType to check constraints for. + * @return bool true if the $nodeType is allowed as grandchild node, false otherwise. + * @throws \InvalidArgumentException if the requested tethered node is not configured in the parent NodeType. Check via {@see NodeType::hasTetheredNode()}. + */ + public function isNodeTypeAllowedAsChildToTetheredNode(NodeType $parentNodeType, NodeName $tetheredNodeName, NodeType $nodeType): bool + { + try { + $typeOfTetheredNode = $this->getTypeOfTetheredNode($parentNodeType, $tetheredNodeName); + } catch (TetheredNodeNotConfigured $exception) { + throw new \InvalidArgumentException( + sprintf( + 'Cannot determine if grandchild is allowed in %s. Because the given child node name "%s" is not auto-created.', + $parentNodeType->name->value, + $tetheredNodeName->value + ), + 1403858395, + $exception + ); + } + + // Constraints configured on the NodeType for the child node + $constraints = $typeOfTetheredNode->getConfiguration('constraints.nodeTypes') ?: []; + + // Constraints configured at the childNode configuration of the parent. + try { + $childNodeConstraintConfiguration = $parentNodeType->getConfiguration('childNodes.' . $tetheredNodeName->value . '.constraints.nodeTypes') ?? []; + } catch (PropertyNotAccessibleException $exception) { + // We ignore this because the configuration might just not have any constraints, if the childNode was not configured the exception above would have been thrown. + $childNodeConstraintConfiguration = []; + } + + $constraints = Arrays::arrayMergeRecursiveOverrule($constraints, $childNodeConstraintConfiguration); + + return (new ConstraintCheck($constraints))->isNodeTypeAllowed($nodeType); + } + /** * Load one node type, if it is not loaded yet. * @@ -238,7 +316,6 @@ protected function loadNodeType(string $nodeTypeName, array &$completeNodeTypeCo NodeTypeName::fromString($nodeTypeName), $superTypes, $nodeTypeConfiguration, - $this, $this->nodeLabelGeneratorFactory ); diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php index 60dfb8741bd..0592c8e3d44 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeManagerTest.php @@ -12,6 +12,8 @@ */ use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory; +use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\SharedModel\Exception\NodeConfigurationException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeIsFinalException; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; @@ -121,6 +123,11 @@ protected function prepareNodeTypeManager(array $nodeTypesFixtureData) ], 'Neos.ContentRepository.Testing:Page2' => [ 'superTypes' => ['Neos.ContentRepository.Testing:Document' => true], + 'childNodes' => [ + 'nodeName' => [ + 'type' => 'Neos.ContentRepository.Testing:Document' + ] + ] ], 'Neos.ContentRepository.Testing:Page3' => [ 'superTypes' => ['Neos.ContentRepository.Testing:Document' => true], @@ -335,4 +342,15 @@ public function getSubNodeTypesWithDifferentIncludeFlagValuesReturnsCorrectValue $subNodeTypes = $this->nodeTypeManager->getSubNodeTypes('Neos.ContentRepository.Testing:ContentObject', false); self::assertArrayNotHasKey('Neos.ContentRepository.Testing:AbstractType', $subNodeTypes); } + + /** + * @test + */ + public function getAutoCreatedChildNodesReturnsLowercaseNames() + { + $parentNodeType = $this->nodeTypeManager->getNodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Page2')); + $autoCreatedChildNodes = $this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($parentNodeType); + // This is configured as "nodeName" above, but should be normalized to "nodename" + self::assertArrayHasKey('nodename', $autoCreatedChildNodes); + } } diff --git a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php index bd38fc10066..3777e0c725c 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/NodeType/NodeTypeTest.php @@ -22,7 +22,6 @@ * Testcase for the "NodeType" domain model */ class NodeTypeTest extends TestCase - { /** * example node types @@ -130,9 +129,7 @@ class NodeTypeTest extends TestCase */ public function aNodeTypeHasAName() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], [], - $this->getMockBuilder(NodeTypeManager::class) - ->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository.Testing:Text'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame('Neos.ContentRepository.Testing:Text', $nodeType->name->value); } @@ -142,9 +139,7 @@ public function aNodeTypeHasAName() public function setDeclaredSuperTypesExpectsAnArrayOfNodeTypesAsKeys() { $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo' => true], [], - $this->getMockBuilder(NodeTypeManager::class) - ->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo' => true], [], new DefaultNodeLabelGeneratorFactory() ); } @@ -154,8 +149,7 @@ public function setDeclaredSuperTypesExpectsAnArrayOfNodeTypesAsKeys() public function setDeclaredSuperTypesAcceptsAnArrayOfNodeTypes() { $this->expectException(\InvalidArgumentException::class); - new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo'], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + new NodeType(NodeTypeName::fromString('ContentRepository:Folder'), ['foo'], [], new DefaultNodeLabelGeneratorFactory()); } /** @@ -163,13 +157,11 @@ public function setDeclaredSuperTypesAcceptsAnArrayOfNodeTypes() */ public function nodeTypesCanHaveAnyNumberOfSuperTypes() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); $timeableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:TimeableContent'), - [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], [], new DefaultNodeLabelGeneratorFactory() ); $documentType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Document'), @@ -177,13 +169,12 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository:Base' => $baseType, 'Neos.ContentRepository.Testing:TimeableContent' => $timeableNodeType, ], - [], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], new DefaultNodeLabelGeneratorFactory() ); $hideableNodeType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:HideableContent'), - [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() + [], [], new DefaultNodeLabelGeneratorFactory() ); $pageType = new NodeType( NodeTypeName::fromString('Neos.ContentRepository.Testing:Page'), @@ -193,7 +184,6 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() 'Neos.ContentRepository.Testing:TimeableContent' => null, ], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() ); @@ -219,8 +209,7 @@ public function nodeTypesCanHaveAnyNumberOfSuperTypes() */ public function labelIsEmptyStringByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame('', $baseType->getLabel()); } @@ -229,8 +218,7 @@ public function labelIsEmptyStringByDefault() */ public function propertiesAreEmptyArrayByDefault() { - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertSame([], $baseType->getProperties()); } @@ -243,7 +231,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + ], new DefaultNodeLabelGeneratorFactory()); self::assertTrue($nodeType->hasConfiguration('someKey.someSubKey')); } @@ -252,8 +240,7 @@ public function hasConfigurationReturnsTrueIfSpecifiedConfigurationPathExists() */ public function hasConfigurationReturnsFalseIfSpecifiedConfigurationPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertFalse($nodeType->hasConfiguration('some.nonExisting.path')); } @@ -266,7 +253,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() 'someKey' => [ 'someSubKey' => 'someValue' ] - ], $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + ], new DefaultNodeLabelGeneratorFactory()); self::assertSame('someValue', $nodeType->getConfiguration('someKey.someSubKey')); } @@ -275,8 +262,7 @@ public function getConfigurationReturnsTheConfigurationWithTheSpecifiedPath() */ public function getConfigurationReturnsNullIfTheSpecifiedPathDoesNotExist() { - $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory()); + $nodeType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [], new DefaultNodeLabelGeneratorFactory()); self::assertNull($nodeType->getConfiguration('some.nonExisting.path')); } @@ -401,25 +387,7 @@ protected function getNodeType(string $nodeTypeName): ?NodeType NodeTypeName::fromString($nodeTypeName), $declaredSuperTypes, $configuration, - $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(), new DefaultNodeLabelGeneratorFactory() ); } - - /** - * @test - */ - public function getAutoCreatedChildNodesReturnsLowercasePaths() - { - $childNodeConfiguration = ['type' => 'Neos.ContentRepository:Base']; - $mockNodeTypeManager = $this->getMockBuilder(NodeTypeManager::class)->disableOriginalConstructor()->getMock(); - $baseType = new NodeType(NodeTypeName::fromString('Neos.ContentRepository:Base'), [], [ - 'childNodes' => ['nodeName' => $childNodeConfiguration] - ], $mockNodeTypeManager, new DefaultNodeLabelGeneratorFactory()); - $mockNodeTypeManager->expects(self::any())->method('getNodeType')->will(self::returnValue($baseType)); - - $autoCreatedChildNodes = $mockNodeTypeManager->getNodeType('Neos.ContentRepository:Base')->getAutoCreatedChildNodes(); - - self::assertArrayHasKey('nodename', $autoCreatedChildNodes); - } } diff --git a/Neos.ContentRepository.Export/src/ImportServiceFactory.php b/Neos.ContentRepository.Export/src/ImportServiceFactory.php index bd140489c1a..01dcd295bbd 100644 --- a/Neos.ContentRepository.Export/src/ImportServiceFactory.php +++ b/Neos.ContentRepository.Export/src/ImportServiceFactory.php @@ -25,8 +25,7 @@ public function __construct( private readonly ResourceRepository $resourceRepository, private readonly ResourceManager $resourceManager, private readonly PersistenceManagerInterface $persistenceManager, - ) - { + ) { } public function build(ContentRepositoryServiceFactoryDependencies $serviceFactoryDependencies): ImportService diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php index b72b8e40d0a..2bcebc44a0e 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationService.php @@ -58,8 +58,7 @@ public function __construct( private readonly PropertyConverter $propertyConverter, private readonly EventStoreInterface $eventStore, private readonly ContentStreamId $contentStreamId, - ) - { + ) { } public function runAllProcessors(\Closure $outputLineFn, bool $verbose = false): void diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php index b8b2ba82655..67bd7df05c5 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/LegacyMigrationServiceFactory.php @@ -41,8 +41,7 @@ public function __construct( private readonly ResourceManager $resourceManager, private readonly PropertyMapper $propertyMapper, private readonly ContentStreamId $contentStreamId, - ) - { + ) { } public function build( diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index ba5440ffc16..9b4ed5e4189 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php +++ b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php @@ -46,14 +46,13 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; -use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\Flow\Persistence\Doctrine\DataTypes\JsonArrayType; use Neos\Flow\Property\PropertyMapper; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Ramsey\Uuid\Uuid; use Webmozart\Assert\Assert; @@ -88,7 +87,7 @@ public function __construct( private readonly Filesystem $files, private readonly iterable $nodeDataRows, ) { - $this->sitesNodeTypeName = NodeTypeName::fromString('Neos.Neos:Sites'); + $this->sitesNodeTypeName = NodeTypeNameFactory::forSites(); $this->contentStreamId = ContentStreamId::create(); $this->visitedNodes = new VisitedNodeAggregates(); } @@ -100,6 +99,13 @@ public function setContentStreamId(ContentStreamId $contentStreamId): void public function setSitesNodeType(NodeTypeName $nodeTypeName): void { + $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); + if (!$nodeType->isOfType(NodeTypeNameFactory::NAME_SITES)) { + throw new \InvalidArgumentException( + sprintf('Sites NodeType "%s" must be of type "%s"', $nodeTypeName->value, NodeTypeNameFactory::NAME_SITES), + 1695802415 + ); + } $this->sitesNodeTypeName = $nodeTypeName; } @@ -244,6 +250,13 @@ public function processNodeDataWithoutFallbackToEmptyDimension(NodeAggregateId $ $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); $serializedPropertyValuesAndReferences = $this->extractPropertyValuesAndReferences($nodeDataRow, $nodeType); + $isSiteNode = $nodeDataRow['parentpath'] === '/sites'; + if ($isSiteNode && !$nodeType->isOfType(NodeTypeNameFactory::NAME_SITE)) { + throw new MigrationException(sprintf( + 'The site node "%s" (type: "%s") must be of type "%s"', $nodeDataRow['identifier'], $nodeTypeName->value, NodeTypeNameFactory::NAME_SITE + ), 1695801620); + } + if ($this->isAutoCreatedChildNode($parentNodeAggregate->nodeTypeName, $nodeName) && !$this->visitedNodes->containsNodeAggregate($nodeAggregateId)) { // Create tethered node if the node was not found before. // If the node was already visited, we want to create a node variant (and keep the tethering status) @@ -393,7 +406,7 @@ private function isAutoCreatedChildNode(NodeTypeName $parentNodeTypeName, NodeNa return false; } $nodeTypeOfParent = $this->nodeTypeManager->getNodeType($parentNodeTypeName); - return $nodeTypeOfParent->hasAutoCreatedChildNode($nodeName); + return $nodeTypeOfParent->hasTetheredNode($nodeName); } private function dispatch(Severity $severity, string $message, mixed ...$args): void diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature index 220bda3b208..bc5cf9671ea 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Assets.feature @@ -8,7 +8,10 @@ Feature: Export of used Assets, Image Variants and Persistent Resources And using the following node types: """yaml 'unstructured': {} - 'Some.Package:SomeNodeType': + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true properties: 'string': type: string @@ -43,7 +46,7 @@ Feature: Export of used Assets, Image Variants and Persistent Resources When I have the following node data rows: | Identifier | Path | Node Type | Properties | | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"string": "asset:\/\/variant1"} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/variant1"} | And I run the asset migration Then I expect the following Assets to be exported: """ @@ -87,10 +90,10 @@ Feature: Export of used Assets, Image Variants and Persistent Resources When I have the following node data rows: | Identifier | Path | Node Type | Dimension Values | Properties | | sites-node-id | /sites | unstructured | | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | | {"string": "asset:\/\/asset1"} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["ch"]} | {"image": {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["en"]} | {"assets": [{"__flow_object_type": "Neos\\Media\\Domain\\Model\\Document", "__identifier": "asset3"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\ImageVariant", "__identifier": "variant1"}]} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["de"]} | {"string": "some text with an asset link"} | + | site-node-id | /sites/test-site | Some.Package:Homepage | | {"string": "asset:\/\/asset1"} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | {"image": {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["en"]} | {"assets": [{"__flow_object_type": "Neos\\Media\\Domain\\Model\\Document", "__identifier": "asset3"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\Image", "__identifier": "asset2"}, {"__flow_object_type": "Neos\\Media\\Domain\\Model\\ImageVariant", "__identifier": "variant1"}]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | {"string": "some text with an asset link"} | And I run the asset migration Then I expect the following Assets to be exported: """ @@ -178,10 +181,10 @@ Feature: Export of used Assets, Image Variants and Persistent Resources When I have the following node data rows: | Identifier | Path | Node Type | Properties | | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"string": "asset:\/\/non-existing-asset"} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"string": "asset:\/\/non-existing-asset"} | And I run the asset migration Then I expect no Assets to be exported And I expect no ImageVariants to be exported And I expect no PersistentResources to be exported And I expect the following errors to be logged - | Failed to extract assets of property "string" of node "site-node-id" (type: "Some.Package:SomeNodeType"): Failed to find mock asset with id "non-existing-asset" | + | Failed to extract assets of property "string" of node "site-node-id" (type: "Some.Package:Homepage"): Failed to find mock asset with id "non-existing-asset" | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature index 20462bc12b0..3623e2fe61a 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Basic.feature @@ -5,7 +5,11 @@ Feature: Simple migrations without content dimensions Given using no content dimensions And using the following node types: """yaml - 'Some.Package:SomeNodeType': + 'unstructured': {} + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true properties: 'text': type: string @@ -18,9 +22,9 @@ Feature: Simple migrations without content dimensions When I have the following node data rows: | Identifier | Path | Node Type | Properties | | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"text": "foo"} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"text": "foo"} | And I run the event migration for content stream "cs-id" Then I expect the following events to be exported | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:SomeNodeType", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | + | NodeAggregateWithNodeWasCreated | {"contentStreamId": "cs-id", "nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular", "initialPropertyValues": {"text": {"type": "string", "value": "foo"}}} | diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature index c036cb2bf52..4cf57e96a4c 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Errors.feature @@ -5,13 +5,22 @@ Feature: Exceptional cases during migrations Given using no content dimensions And using the following node types: """yaml - 'unstructured': [] - 'Some.Package:SomeNodeType': + 'unstructured': {} + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true properties: 'text': type: string defaultValue: 'My default text' - 'Some.Package:SomeOtherNodeType': [] + 'Some.Package:SomeNodeType': + properties: + 'text': + type: string + 'Some.Package:SomeOtherHomepage': + superTypes: + 'Neos.Neos:Site': true """ And using identifier "default", I define a content repository And I am in content repository "default" @@ -23,26 +32,26 @@ Feature: Exceptional cases during migrations When I have the following node data rows: | Identifier | Path | Node Type | Dimension Values | | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["de"]} | - | site-node-id | /sites/test-site | Some.Package:SomeOtherNodeType | {"language": ["en"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | + | site-node-id | /sites/test-site | Some.Package:SomeOtherHomepage | {"language": ["en"]} | And I run the event migration Then I expect a MigrationError with the message """ - Node aggregate with id "site-node-id" has a type of "Some.Package:SomeOtherNodeType" in content dimension [{"language":"en"}]. I was visited previously for content dimension [{"language":"de"}] with the type "Some.Package:SomeNodeType". Node variants must not have different types + Node aggregate with id "site-node-id" has a type of "Some.Package:SomeOtherHomepage" in content dimension [{"language":"en"}]. I was visited previously for content dimension [{"language":"de"}] with the type "Some.Package:Homepage". Node variants must not have different types """ # Note: The behavior was changed with https://github.com/neos/neos-development-collection/pull/4201 Scenario: Node with missing parent When I have the following node data rows: - | Identifier | Path | - | sites | /sites | - | a | /sites/a | - | c | /sites/b/c | + | Identifier | Path | Node Type | + | sites | /sites | unstructured | + | a | /sites/a | Some.Package:Homepage | + | c | /sites/b/c | unstructured | And I run the event migration Then I expect the following events to be exported | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "sites"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "nodeTypeName": "Some.Package:Homepage", "parentNodeAggregateId": "sites"} | And I expect the following errors to be logged | Failed to find parent node for node with id "c" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | @@ -50,17 +59,17 @@ Feature: Exceptional cases during migrations # Note: The behavior was changed with https://github.com/neos/neos-development-collection/pull/4201 Scenario: Nodes out of order When I have the following node data rows: - | Identifier | Path | - | sites | /sites | - | a | /sites/a | - | c | /sites/b/c | - | b | /sites/b | + | Identifier | Path | Node Type | + | sites | /sites | unstructured | + | a | /sites/a | Some.Package:Homepage | + | c | /sites/b/c | unstructured | + | b | /sites/b | Some.Package:Homepage | And I run the event migration Then I expect the following events to be exported | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites", "nodeTypeName": "Neos.Neos:Sites", "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "parentNodeAggregateId": "sites"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "parentNodeAggregateId": "sites"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "a", "nodeTypeName": "Some.Package:Homepage", "parentNodeAggregateId": "sites"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "b", "nodeTypeName": "Some.Package:Homepage", "parentNodeAggregateId": "sites"} | And I expect the following errors to be logged | Failed to find parent node for node with id "c" and dimensions: []. Please ensure that the new content repository has a valid content dimension configuration. Also note that the old CR can sometimes have orphaned nodes. | @@ -104,11 +113,11 @@ Feature: Exceptional cases during migrations | Identifier | Default | Values | Generalizations | | language | en | en, de, ch | ch->de | When I have the following node data rows: - | Identifier | Path | Dimension Values | - | sites-node-id | /sites | | - | site-node-id | /sites/test-site | {"language": ["de"]} | - | site-node-id | /sites/test-site | {"language": ["ch"]} | - | site-node-id | /sites/test-site | {"language": ["ch"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites-node-id | /sites | unstructured | | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | And I run the event migration Then I expect a MigrationError with the message """ @@ -120,12 +129,23 @@ Feature: Exceptional cases during migrations | Identifier | Default | Values | Generalizations | | language | en | en, de, ch | ch->de | When I have the following node data rows: - | Identifier | Path | Dimension Values | - | sites-node-id | /sites | | - | site-node-id | /sites/test-site | {"language": ["de"]} | - | site-node-id | /sites/test-site | {"language": ["de"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites-node-id | /sites | unstructured | | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | And I run the event migration Then I expect a MigrationError with the message """ Node "site-node-id" for dimension {"language":"de"} was already created previously """ + + Scenario: Homepage node is not of type "Neos.Neos:Site" + When I have the following node data rows: + | Identifier | Path | Node Type | + | sites-node-id | /sites | unstructured | + | site-node-id | /sites/test-site | unstructured | + And I run the event migration + Then I expect a MigrationError with the message + """ + The site node "site-node-id" (type: "unstructured") must be of type "Neos.Neos:Site" + """ diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature index e9dcb7dfc60..7c67812eb50 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/References.feature @@ -5,7 +5,11 @@ Feature: Migrations that contain nodes with "reference" or "references propertie Given using no content dimensions And using the following node types: """yaml - 'Some.Package:Homepage': [] + 'unstructured': {} + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true 'Some.Package:SomeNodeType': properties: 'text': diff --git a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature index f5a1299ec0b..e6285a44323 100644 --- a/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature +++ b/Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/Features/Variants.feature @@ -8,27 +8,26 @@ Feature: Migrating nodes with content dimensions And using the following node types: """yaml 'unstructured': {} - 'Some.Package:SomeNodeType': - properties: - 'text': - type: string - defaultValue: 'My default text' + 'Neos.Neos:Site': {} + 'Some.Package:Homepage': + superTypes: + 'Neos.Neos:Site': true """ And using identifier "default", I define a content repository And I am in content repository "default" Scenario: Node specialization variants are prioritized over peer variants When I have the following node data rows: - | Identifier | Path | Node Type | Dimension Values | - | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["de"]} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["en"]} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["ch"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites-node-id | /sites | unstructured | | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["en"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | And I run the event migration Then I expect the following events to be exported | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "coveredDimensionSpacePoints": [{"language": "en"},{"language": "de"},{"language": "ch"}], "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:SomeNodeType", "nodeName": "test-site", "originDimensionSpacePoint": {"language": "de"}, "coveredDimensionSpacePoints": [{"language": "de"},{"language": "ch"}], "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "originDimensionSpacePoint": {"language": "de"}, "coveredDimensionSpacePoints": [{"language": "de"},{"language": "ch"}], "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular"} | | NodePeerVariantWasCreated | {"nodeAggregateId": "site-node-id", "sourceOrigin": {"language": "de"}, "peerOrigin": {"language": "en"}, "peerCoverage": [{"language": "en"}]} | | NodeSpecializationVariantWasCreated | {"nodeAggregateId": "site-node-id", "sourceOrigin": {"language": "de"}, "specializationOrigin": {"language": "ch"}, "specializationCoverage": [{"language": "ch"}]} | @@ -36,14 +35,14 @@ Feature: Migrating nodes with content dimensions When I have the following node data rows: | Identifier | Path | Node Type | Dimension Values | | sites-node-id | /sites | unstructured | | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["ch"]} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["en"]} | - | site-node-id | /sites/test-site | Some.Package:SomeNodeType | {"language": ["de"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["ch"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["en"]} | + | site-node-id | /sites/test-site | Some.Package:Homepage | {"language": ["de"]} | And I run the event migration Then I expect the following events to be exported | Type | Payload | | RootNodeAggregateWithNodeWasCreated | {"nodeAggregateId": "sites-node-id", "nodeTypeName": "Neos.Neos:Sites", "coveredDimensionSpacePoints": [{"language": "en"},{"language": "de"},{"language": "ch"}], "nodeAggregateClassification": "root"} | - | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:SomeNodeType", "nodeName": "test-site", "originDimensionSpacePoint": {"language": "ch"}, "coveredDimensionSpacePoints": [{"language": "ch"}], "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular"} | + | NodeAggregateWithNodeWasCreated | {"nodeAggregateId": "site-node-id", "nodeTypeName": "Some.Package:Homepage", "nodeName": "test-site", "originDimensionSpacePoint": {"language": "ch"}, "coveredDimensionSpacePoints": [{"language": "ch"}], "parentNodeAggregateId": "sites-node-id", "nodeAggregateClassification": "regular"} | | NodePeerVariantWasCreated | {"nodeAggregateId": "site-node-id", "sourceOrigin": {"language": "ch"}, "peerOrigin": {"language": "en"}, "peerCoverage": [{"language": "en"}]} | | NodeGeneralizationVariantWasCreated | {"nodeAggregateId": "site-node-id", "sourceOrigin": {"language": "ch"}, "generalizationOrigin": {"language": "de"}, "generalizationCoverage": [{"language": "de"}]} | @@ -52,10 +51,10 @@ Feature: Migrating nodes with content dimensions | Identifier | Default | Values | Generalizations | | language | mul | mul, en, de, ch | ch->de->mul | When I have the following node data rows: - | Identifier | Path | Dimension Values | - | sites | /sites | | - | site | /sites/site | {"language": ["mul"]} | - | site | /sites/site | {"language": ["de"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites | /sites | unstructured | | + | site | /sites/site | Some.Package:Homepage | {"language": ["mul"]} | + | site | /sites/site | Some.Package:Homepage | {"language": ["de"]} | And I run the event migration Then I expect the following events to be exported | Type | Payload | @@ -65,13 +64,13 @@ Feature: Migrating nodes with content dimensions Scenario: Node variant with different parent node (moved) When I have the following node data rows: - | Identifier | Path | Dimension Values | - | sites | /sites | | - | site | /sites/site | {"language": ["de"]} | - | a | /sites/site/a | {"language": ["de"]} | - | a1 | /sites/site/a/a1 | {"language": ["de"]} | - | b | /sites/site/b | {"language": ["de"]} | - | a1 | /sites/site/b/a1 | {"language": ["ch"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites | /sites | unstructured | | + | site | /sites/site | Some.Package:Homepage | {"language": ["de"]} | + | a | /sites/site/a | unstructured | {"language": ["de"]} | + | a1 | /sites/site/a/a1 | unstructured | {"language": ["de"]} | + | b | /sites/site/b | unstructured | {"language": ["de"]} | + | a1 | /sites/site/b/a1 | unstructured | {"language": ["ch"]} | And I run the event migration Then I expect the following events to be exported | Type | Payload | @@ -86,14 +85,14 @@ Feature: Migrating nodes with content dimensions Scenario: Node variant with different grand parent node (ancestor node was moved) - Note: There is only NodeAggregateWasMoved event for "a" and not for "a1" When I have the following node data rows: - | Identifier | Path | Dimension Values | - | sites | /sites | | - | site | /sites/site | {"language": ["de"]} | - | a | /sites/site/a | {"language": ["de"]} | - | a1 | /sites/site/a/a1 | {"language": ["de"]} | - | b | /sites/site/b | {"language": ["de"]} | - | a | /sites/site/b/a | {"language": ["ch"]} | - | a1 | /sites/site/b/a/a1 | {"language": ["ch"]} | + | Identifier | Path | Node Type | Dimension Values | + | sites | /sites | unstructured | | + | site | /sites/site | Some.Package:Homepage | {"language": ["de"]} | + | a | /sites/site/a | unstructured | {"language": ["de"]} | + | a1 | /sites/site/a/a1 | unstructured | {"language": ["de"]} | + | b | /sites/site/b | unstructured | {"language": ["de"]} | + | a | /sites/site/b/a | unstructured | {"language": ["ch"]} | + | a1 | /sites/site/b/a/a1 | unstructured | {"language": ["ch"]} | And I run the event migration Then I expect the following events to be exported | Type | Payload | diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php index dcf0d4233c3..2310ee2d59f 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php @@ -61,7 +61,6 @@ public function canEvaluate($context) * * @param FlowQuery $flowQuery the FlowQuery object * @param array $arguments the arguments for this operation - * @todo Compare to node type Neos.Neos:Site instead of path once it is available * @return void */ public function evaluate(FlowQuery $flowQuery, array $arguments) diff --git a/Neos.ContentRepository.NodeMigration/src/Filter/DimensionSpacePointsFilterFactory.php b/Neos.ContentRepository.NodeMigration/src/Filter/DimensionSpacePointsFilterFactory.php index c6b072c2a0d..1c05d28cafc 100644 --- a/Neos.ContentRepository.NodeMigration/src/Filter/DimensionSpacePointsFilterFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Filter/DimensionSpacePointsFilterFactory.php @@ -14,7 +14,6 @@ namespace Neos\ContentRepository\NodeMigration\Filter; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; use Neos\ContentRepository\Core\DimensionSpace\VariantType; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePointSet; diff --git a/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php b/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php index 57296c6aa65..ccf503359f6 100644 --- a/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php +++ b/Neos.ContentRepository.NodeMigration/src/NodeMigrationService.php @@ -14,7 +14,6 @@ use Neos\ContentRepository\NodeMigration\Transformation\TransformationsFactory; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateWorkspace; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceDescription; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceTitle; @@ -54,8 +53,7 @@ public function __construct( private readonly ContentRepository $contentRepository, private readonly FiltersFactory $filterFactory, private readonly TransformationsFactory $transformationFactory - ) - { + ) { } public function executeMigration(ExecuteMigration $command): void @@ -98,8 +96,7 @@ protected function executeSubMigrationAndBlock( array $migrationDescription, ContentStreamId $contentStreamForReading, ContentStreamId $contentStreamForWriting - ): void - { + ): void { $filters = $this->filterFactory->buildFilterConjunction($migrationDescription['filters'] ?? []); $transformations = $this->transformationFactory->buildTransformation( $migrationDescription['transformations'] ?? [] diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php index 72a23fd9db8..c40da53c40a 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/AddNewPropertyTransformationFactory.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; -use Neos\ContentRepository\Core\SharedModel\User\UserId; class AddNewPropertyTransformationFactory implements TransformationFactoryInterface { diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php index 13eed059459..ca1b472827a 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/ChangeNodeTypeTransformationFactory.php @@ -20,7 +20,6 @@ use Neos\ContentRepository\Core\Feature\NodeTypeChange\Dto\NodeAggregateTypeChangeChildConstraintConflictResolutionStrategy; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\NodeType\NodeTypeName; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; /** @codingStandardsIgnoreStart */ diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php index 6b0e2595c16..bd97abc0574 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RemoveNodeTransformationFactory.php @@ -22,7 +22,6 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\User\UserId; /** * Remove Node diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php index 0a362e82dad..783cfa7a5f6 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/RenameNodeAggregateTransformationFactory.php @@ -19,7 +19,6 @@ use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; class RenameNodeAggregateTransformationFactory implements TransformationFactoryInterface diff --git a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php index 3f0c1271ed8..66143d2ee4e 100644 --- a/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php +++ b/Neos.ContentRepository.NodeMigration/src/Transformation/TransformationsFactory.php @@ -20,8 +20,7 @@ class TransformationsFactory public function __construct( private readonly ContentRepository $contentRepository - ) - { + ) { } public function registerTransformation(string $transformationIdentifier, TransformationFactoryInterface $transformationFactory): self diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php index a8b1c362855..d19b762ace8 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/DisallowedChildNodeAdjustment.php @@ -62,7 +62,6 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat ? $subgraph->findParentNode($parentNode->nodeAggregateId) : null; - $allowedByParent = true; $parentNodeType = null; if ($parentNode !== null) { @@ -76,14 +75,15 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $grandparentNodeType = null; if ( $parentNode !== null - && $grandparentNode != null + && $grandparentNode !== null && $parentNode->classification->isTethered() && !is_null($parentNode->nodeName) ) { - if ($this->nodeTypeManager->hasNodeType($grandparentNode->nodeTypeName)) { - $grandparentNodeType = $this->nodeTypeManager->getNodeType($grandparentNode->nodeTypeName); - $allowedByGrandparent = $grandparentNodeType->allowsGrandchildNodeType( - $parentNode->nodeName->value, + $grandparentNodeType = $this->nodeTypeManager->hasNodeType($grandparentNode->nodeTypeName) ? $this->nodeTypeManager->getNodeType($grandparentNode->nodeTypeName) : null; + if ($grandparentNodeType !== null) { + $allowedByGrandparent = $this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode( + $grandparentNodeType, + $parentNode->nodeName, $nodeType ); } @@ -97,10 +97,10 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat $message = sprintf( ' - The parent node type "%s" is not allowing children of type "%s", - and the grandparent node type "%s" is not allowing grandchildren of type "%s". - Thus, the node is invalid at this location and should be removed. - ', + The parent node type "%s" is not allowing children of type "%s", + and the grandparent node type "%s" is not allowing grandchildren of type "%s". + Thus, the node is invalid at this location and should be removed. + ', $parentNodeType !== null ? $parentNodeType->name->value : '', $node->nodeTypeName->value, $grandparentNodeType !== null ? $grandparentNodeType->name->value : '', @@ -123,6 +123,7 @@ function () use ($nodeAggregate, $coveredDimensionSpacePoint) { } } + private function removeNodeInSingleDimensionSpacePoint( NodeAggregate $nodeAggregate, DimensionSpacePoint $dimensionSpacePoint diff --git a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php index aa50bb0324b..994672b390e 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php +++ b/Neos.ContentRepository.StructureAdjustment/src/Adjustment/TetheredNodeAdjustments.php @@ -51,9 +51,7 @@ public function findAdjustmentsForNodeType(NodeTypeName $nodeTypeName): \Generat // In case we cannot find the expected tethered nodes, this fix cannot do anything. return; } - $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); - - $expectedTetheredNodes = $nodeType->getAutoCreatedChildNodes(); + $expectedTetheredNodes = $this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($this->nodeTypeManager->getNodeType($nodeTypeName)); foreach ($this->projectedNodeIterator->nodeAggregatesOfType($nodeTypeName) as $nodeAggregate) { // find missing tethered nodes diff --git a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php index 736a9141299..a6fe7ec2018 100644 --- a/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php +++ b/Neos.ContentRepository.StructureAdjustment/src/StructureAdjustmentService.php @@ -32,8 +32,7 @@ public function __construct( private readonly EventPersister $eventPersister, NodeTypeManager $nodeTypeManager, InterDimensionalVariationGraph $interDimensionalVariationGraph, - ) - { + ) { $projectedNodeIterator = new ProjectedNodeIterator( $contentRepository->getWorkspaceFinder(), $contentRepository->getContentGraph(), diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/NodeMigrationCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/NodeMigrationCommandController.php index fdcd69d3a96..86955520f8f 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/NodeMigrationCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/NodeMigrationCommandController.php @@ -40,8 +40,7 @@ public function __construct( private readonly ContentRepositoryRegistry $contentRepositoryRegistry, private readonly PackageManager $packageManager, private readonly NodeMigrationGeneratorService $nodeMigrationGeneratorService - ) - { + ) { parent::__construct(); } @@ -98,20 +97,51 @@ public function executeCommand(string $version, string $workspace = 'live', bool */ public function kickstartCommand(string $packageKey): void { - if (!$this->packageManager->isPackageAvailable($packageKey)) { - $this->outputLine('Package "%s" is not available.', [$packageKey]); - $this->quit(1); + if (!$this->packageManager->isPackageAvailable($packageKey)) { + $this->outputLine('Package "%s" is not available.', [$packageKey]); + $this->quit(1); } try { $createdMigration = $this->nodeMigrationGeneratorService->generateBoilerplateMigrationFileInPackage($packageKey); } catch (MigrationException $e) { - $this->outputLine(); - $this->outputLine('Error ' . $e->getMessage()); - $this->quit(1); + $this->outputLine(); + $this->outputLine('Error ' . $e->getMessage()); + $this->quit(1); + } + $this->outputLine($createdMigration); + $this->outputLine('Your node migration has been created successfully.'); + } + + /** + * List available migrations + * + * @see neos.contentrepositoryregistry:nodemigration:list + */ + public function listCommand(): void + { + $availableMigrations = $this->migrationFactory->getAvailableVersions(); + if (count($availableMigrations) === 0) { + $this->outputLine('No migrations available.'); + $this->quit(); + } + + $tableRows = []; + foreach ($availableMigrations as $version => $migration) { + $migrationUpConfigurationComments = $this->migrationFactory->getMigrationForVersion($version)->getComments(); + + $tableRows[] = [ + $version, + $migration['formattedVersionNumber'], + $migration['package']->getPackageKey(), + + wordwrap($migrationUpConfigurationComments, 60) + ]; } - $this->outputLine($createdMigration); - $this->outputLine('Your node migration has been created successfully.'); + + $this->outputLine('Available migrations'); + $this->outputLine(); + $this->output->outputTable($tableRows, ['Version', 'Date', 'Package', 'Comments']); } /** diff --git a/Neos.ContentRepositoryRegistry/Classes/Configuration/NodeTypeEnrichmentService.php b/Neos.ContentRepositoryRegistry/Classes/Configuration/NodeTypeEnrichmentService.php index 5c09351afb2..57621fc6f9c 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Configuration/NodeTypeEnrichmentService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Configuration/NodeTypeEnrichmentService.php @@ -178,8 +178,7 @@ protected function applyEditorLabels( $editorName, array &$editorOptions, $translationIdGenerator - ) - { + ) { switch ($editorName) { case 'Neos.Neos/Inspector/Editors/SelectBoxEditor': if ($this->shouldFetchTranslation($editorOptions, 'placeholder')) { diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php index 6dbafd9d2b4..e74bfb15368 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/EventStore/DoctrineEventStoreFactory.php @@ -13,8 +13,7 @@ class DoctrineEventStoreFactory implements EventStoreFactoryInterface { public function __construct( private readonly Connection $connection, - ) - { + ) { } public function build(ContentRepositoryId $contentRepositoryId, array $options, ClockInterface $clock): EventStoreInterface diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php b/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php index 3eb010d5c10..1486c4ef1ec 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/NodeTypeManager/DefaultNodeTypeManagerFactory.php @@ -13,8 +13,7 @@ public function __construct( private ConfigurationManager $configurationManager, private ObjectManagerBasedNodeLabelGeneratorFactory $nodeLabelGeneratorFactory, private NodeTypeEnrichmentService $nodeTypeEnrichmentService, - ) - { + ) { } public function build(ContentRepositoryId $contentRepositoryId, array $options): NodeTypeManager diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php index f735cc414ea..51be1a2631a 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/CatchUpTriggerWithSynchronousOption.php @@ -57,8 +57,7 @@ public static function synchronously(\Closure $fn): void public function __construct( private readonly ContentRepositoryId $contentRepositoryId, private readonly SubprocessProjectionCatchUpTrigger $inner - ) - { + ) { } public function triggerCatchUp(Projections $projections): void diff --git a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/SubprocessProjectionCatchUpTrigger.php b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/SubprocessProjectionCatchUpTrigger.php index d99d4b9909f..0f63d7c06c9 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/SubprocessProjectionCatchUpTrigger.php +++ b/Neos.ContentRepositoryRegistry/Classes/Factory/ProjectionCatchUpTrigger/SubprocessProjectionCatchUpTrigger.php @@ -22,8 +22,7 @@ class SubprocessProjectionCatchUpTrigger implements ProjectionCatchUpTriggerInte public function __construct( private readonly ContentRepositoryId $contentRepositoryId - ) - { + ) { } public function triggerCatchUp(Projections $projections): void diff --git a/Neos.ContentRepositoryRegistry/Classes/Migration/Factory/MigrationFactory.php b/Neos.ContentRepositoryRegistry/Classes/Migration/Factory/MigrationFactory.php index ad0489ce1ba..7040708b228 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Migration/Factory/MigrationFactory.php +++ b/Neos.ContentRepositoryRegistry/Classes/Migration/Factory/MigrationFactory.php @@ -1,4 +1,5 @@ migrationConfiguration->getMigrationVersion($version)); } + + public function getAvailableVersions(): array + { + return $this->migrationConfiguration->getAvailableVersions(); + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Service/NodeMigrationGeneratorService.php b/Neos.ContentRepositoryRegistry/Classes/Service/NodeMigrationGeneratorService.php index 2313b585842..b96cb1a3226 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Service/NodeMigrationGeneratorService.php +++ b/Neos.ContentRepositoryRegistry/Classes/Service/NodeMigrationGeneratorService.php @@ -1,4 +1,5 @@ getContentGraph()->findNodeByIdAndOriginDimensionSpacePoint( + $subgraph = $contentRepository->getContentGraph()->getSubgraph( $usage->getContentStreamId(), - $usage->getNodeAggregateId(), - $usage->getOriginDimensionSpacePoint() + $usage->getOriginDimensionSpacePoint()->toDimensionSpacePoint(), + VisibilityConstraints::withoutRestrictions() ); + $node = $subgraph->findNodeById($usage->getNodeAggregateId()); // this should actually never happen. if (!$node) { $inaccessibleRelations[] = $inaccessibleRelation; continue; } - $flowQuery = new FlowQuery([$node]); - $documentNode = $flowQuery->closest('[instanceof Neos.Neos:Document]')->get(0); + $documentNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_DOCUMENT)); // this should actually never happen, too. if (!$documentNode) { $inaccessibleRelations[] = $inaccessibleRelation; continue; } - - $siteNode = $this->siteNodeUtility->findSiteNode($node); + $siteNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE)); + // this should actually never happen, too. :D + if (!$siteNode) { + $inaccessibleRelations[] = $inaccessibleRelation; + continue; + } foreach ($existingSites as $existingSite) { /** @var Site $existingSite * */ if ($siteNode->nodeName->equals($existingSite->getNodeName()->toNodeName())) { diff --git a/Neos.Neos/Classes/Command/SiteCommandController.php b/Neos.Neos/Classes/Command/SiteCommandController.php index 5de738b7e24..14354cd743b 100644 --- a/Neos.Neos/Classes/Command/SiteCommandController.php +++ b/Neos.Neos/Classes/Command/SiteCommandController.php @@ -95,7 +95,7 @@ public function createCommand($name, $packageKey, $nodeType, $nodeName = null, $ } catch (SiteNodeTypeIsInvalid $exception) { $this->outputLine( 'The given node type "%s" is not based on the superType "%s"', - [$nodeType, NodeTypeNameFactory::forSite()] + [$nodeType, NodeTypeNameFactory::NAME_SITE] ); $this->quit(1); } catch (SiteNodeNameIsAlreadyInUseByAnotherSite | NodeNameIsAlreadyOccupied $exception) { diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 34c64b2ab69..8f8c450cef2 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -181,10 +181,6 @@ public function previewAction(string $node): void if (!$this->view->canRenderWithNodeAndPath()) { $this->view->setFusionPath('rawContent'); } - - if ($this->session->isStarted()) { - $this->session->putData('lastVisitedNode', $nodeAddress); - } } } diff --git a/Neos.Neos/Classes/Controller/LoginController.php b/Neos.Neos/Classes/Controller/LoginController.php index 43fed8f5ff2..c7601c8b540 100644 --- a/Neos.Neos/Classes/Controller/LoginController.php +++ b/Neos.Neos/Classes/Controller/LoginController.php @@ -109,7 +109,7 @@ public function initializeIndexAction(): void $this->request->setArgument( 'username', $authenticationArgument['Neos']['Flow']['Security']['Authentication'] - ['Token']['UsernamePassword']['username'] + ['Token']['UsernamePassword']['username'] ); } } @@ -173,9 +173,7 @@ public function tokenLoginAction(string $token): void if ($newSession->canBeResumed()) { $newSession->resume(); } - if ($newSession->isStarted()) { - $newSession->putData('lastVisitedNode', null); - } else { + if (!$newSession->isStarted()) { $this->logger->error(sprintf( 'Failed resuming or starting session %s which was referred to in the login token %s.', $newSessionId, @@ -230,12 +228,6 @@ protected function onAuthenticationSuccess(ActionRequest $originalRequest = null ] ); } else { - if ( - $this->request->hasArgument('lastVisitedNode') - && $this->request->getArgument('lastVisitedNode') !== '' - ) { - $this->session->putData('lastVisitedNode', $this->request->getArgument('lastVisitedNode')); - } if ($originalRequest !== null) { // Redirect to the location that redirected to the login form because the user was nog logged in $this->redirectToRequest($originalRequest); @@ -256,16 +248,12 @@ protected function onAuthenticationSuccess(ActionRequest $originalRequest = null */ public function logoutAction(): void { - $possibleRedirectionUri = $this->backendRedirectionService->getAfterLogoutRedirectionUri($this->request); parent::logoutAction(); switch ($this->request->getFormat()) { case 'json': $this->view->assign('value', ['success' => true]); break; default: - if ($possibleRedirectionUri !== null) { - $this->redirectToUri($possibleRedirectionUri); - } $this->addFlashMessage( $this->getLabel('login.loggedOut.body'), $this->getLabel('login.loggedOut.title'), diff --git a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 44c3e8ef1a4..04fecdde89d 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -15,7 +15,6 @@ namespace Neos\Neos\Controller\Module\Administration; use Neos\ContentRepository\Core\Feature\NodeRenaming\Command\ChangeNodeAggregateName; -use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; @@ -234,7 +233,7 @@ public function updateSiteAction(Site $site, $newSiteNodeName) [], 1412371798 ); - $this->unsetLastVisitedNodeAndRedirect('index'); + $this->redirect('index'); } /** @@ -399,7 +398,7 @@ public function createSiteNodeAction($packageKey, $siteName, $nodeType) $this->addFlashMessage( $this->getModuleLabel( 'sites.siteCreationError.givenNodeTypeNotBasedOnSuperType.body', - [$nodeType, NodeTypeNameFactory::forSite()] + [$nodeType, NodeTypeNameFactory::NAME_SITE] ), $this->getModuleLabel('sites.siteCreationError.givenNodeTypeNotBasedOnSuperType.title'), Message::SEVERITY_ERROR, @@ -430,7 +429,7 @@ public function createSiteNodeAction($packageKey, $siteName, $nodeType) [], 1412372266 ); - $this->unsetLastVisitedNodeAndRedirect('index'); + $this->redirect('index'); } /** @@ -450,7 +449,7 @@ public function deleteSiteAction(Site $site) [], 1412372689 ); - $this->unsetLastVisitedNodeAndRedirect('index'); + $this->redirect('index'); } /** @@ -470,7 +469,7 @@ public function activateSiteAction(Site $site) [], 1412372881 ); - $this->unsetLastVisitedNodeAndRedirect('index'); + $this->redirect('index'); } /** @@ -490,7 +489,7 @@ public function deactivateSiteAction(Site $site) [], 1412372975 ); - $this->unsetLastVisitedNodeAndRedirect('index'); + $this->redirect('index'); } /** @@ -525,7 +524,7 @@ public function updateDomainAction(Domain $domain) [], 1412373069 ); - $this->unsetLastVisitedNodeAndRedirect('edit', null, null, ['site' => $domain->getSite()]); + $this->redirect('edit', null, null, ['site' => $domain->getSite()]); } /** @@ -562,7 +561,7 @@ public function createDomainAction(Domain $domain) [], 1412373192 ); - $this->unsetLastVisitedNodeAndRedirect('edit', null, null, ['site' => $domain->getSite()]); + $this->redirect('edit', null, null, ['site' => $domain->getSite()]); } /** @@ -587,7 +586,7 @@ public function deleteDomainAction(Domain $domain) [], 1412373310 ); - $this->unsetLastVisitedNodeAndRedirect('edit', null, null, ['site' => $site]); + $this->redirect('edit', null, null, ['site' => $site]); } /** @@ -608,7 +607,7 @@ public function activateDomainAction(Domain $domain) [], 1412373539 ); - $this->unsetLastVisitedNodeAndRedirect('edit', null, null, ['site' => $domain->getSite()]); + $this->redirect('edit', null, null, ['site' => $domain->getSite()]); } /** @@ -629,31 +628,6 @@ public function deactivateDomainAction(Domain $domain) [], 1412373425 ); - $this->unsetLastVisitedNodeAndRedirect('edit', null, null, ['site' => $domain->getSite()]); - } - - /** - * @param string $actionName Name of the action to forward to - * @param string $controllerName Unqualified object name of the controller to forward to. - * If not specified, the current controller is used. - * @param string $packageKey Key of the package containing the controller to forward to. - * If not specified, the current package is assumed. - * @param array $arguments Array of arguments for the target action - * @param integer $delay (optional) The delay in seconds. Default is no delay. - * @param integer $statusCode (optional) The HTTP status code for the redirect. Default is "303 See Other" - * @param string $format The format to use for the redirect URI - * @return void - */ - protected function unsetLastVisitedNodeAndRedirect( - $actionName, - $controllerName = null, - $packageKey = null, - array $arguments = [], - $delay = 0, - $statusCode = 303, - $format = null - ) { - $this->session->putData('lastVisitedNode', null); - parent::redirect($actionName, $controllerName, $packageKey, $arguments, $delay, $statusCode, $format); + $this->redirect('edit', null, null, ['site' => $domain->getSite()]); } } diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index c197c9f99b2..f688b009f91 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -790,9 +790,9 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos if (is_null($documentNode)) { $documentNode = $ancestor; } - // the site node is the last ancestor of type Document - // TODO: Check for Neos.Neos:Site instead of regular document after it is introduced - $siteNode = $ancestor; + } + if ($this->getNodeType($ancestor)->isOfType(NodeTypeNameFactory::NAME_SITE)) { + $siteNode = $documentNode; } } diff --git a/Neos.Neos/Classes/Domain/Exception/CurrentUserIsMissing.php b/Neos.Neos/Classes/Domain/Exception/CurrentUserIsMissing.php deleted file mode 100644 index 17ee064d9c3..00000000000 --- a/Neos.Neos/Classes/Domain/Exception/CurrentUserIsMissing.php +++ /dev/null @@ -1,29 +0,0 @@ -value - . '" is not of required type "' . NodeTypeNameFactory::forSite()->value . '"', + . '" is not of required type "' . NodeTypeNameFactory::NAME_SITE . '"', 1412372375 ); } diff --git a/Neos.Neos/Classes/Domain/Exception/SitesNodeIsMissing.php b/Neos.Neos/Classes/Domain/Exception/SitesNodeIsMissing.php deleted file mode 100644 index ee2cea42256..00000000000 --- a/Neos.Neos/Classes/Domain/Exception/SitesNodeIsMissing.php +++ /dev/null @@ -1,30 +0,0 @@ -value . '" root node is missing.', - 1651956364 - ); - } -} diff --git a/Neos.Neos/Classes/Domain/Repository/SiteRepository.php b/Neos.Neos/Classes/Domain/Repository/SiteRepository.php index 815d9057028..d097041e88d 100644 --- a/Neos.Neos/Classes/Domain/Repository/SiteRepository.php +++ b/Neos.Neos/Classes/Domain/Repository/SiteRepository.php @@ -15,13 +15,17 @@ namespace Neos\Neos\Domain\Repository; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\QueryInterface; use Neos\Flow\Persistence\QueryResultInterface; use Neos\Flow\Persistence\Repository; -use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Exception as NeosException; +use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Model\SiteNodeName; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Domain\Service\SiteNodeUtility; +use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** * The Site Repository @@ -33,6 +37,11 @@ */ class SiteRepository extends Repository { + use NodeTypeWithFallbackProvider; + + #[Flow\Inject] + protected ContentRepositoryRegistry $contentRepositoryRegistry; + /** * @var array */ @@ -95,8 +104,26 @@ public function findOneByNodeName(string|SiteNodeName $nodeName): ?Site return $site; } + /** + * Finds a given site by site node. + * + * To find the correct site node by its descended child node leverage `findClosestNode`: + * ```php + * $siteNode = $subgraph->findClosestNode( + * $node->nodeAggregateId, + * FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE) + * ); + * ``` + * + * To resolve the SiteNode by a Site use {@see SiteNodeUtility::findSiteNodeBySite()} + * + * @throws \Neos\Neos\Domain\Exception in case the passed $siteNode is not a real site node or no site matches this site node. + */ public function findSiteBySiteNode(Node $siteNode): Site { + if (!$this->getNodeType($siteNode)->isOfType(NodeTypeNameFactory::NAME_SITE)) { + throw new \Neos\Neos\Domain\Exception(sprintf('Node %s is not a site node. Site nodes must be of type "%s".', $siteNode->nodeAggregateId->value, NodeTypeNameFactory::NAME_SITE), 1697108987); + } if ($siteNode->nodeName === null) { throw new \Neos\Neos\Domain\Exception(sprintf('Site node "%s" is unnamed', $siteNode->nodeAggregateId->value), 1681286146); } diff --git a/Neos.Neos/Classes/Domain/Service/NodeSiteResolvingService.php b/Neos.Neos/Classes/Domain/Service/NodeSiteResolvingService.php index 49aa0564179..5fd9c00d611 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeSiteResolvingService.php +++ b/Neos.Neos/Classes/Domain/Service/NodeSiteResolvingService.php @@ -15,21 +15,20 @@ namespace Neos\Neos\Domain\Service; use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\Neos\FrontendRouting\NodeAddress; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Neos\Utility\NodeTypeWithFallbackProvider; +use Neos\Neos\FrontendRouting\NodeAddress; #[Flow\Scope('singleton')] class NodeSiteResolvingService { - use NodeTypeWithFallbackProvider; - #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; + /** @internal */ public function findSiteNodeForNodeAddress( NodeAddress $nodeAddress, ContentRepositoryId $contentRepositoryId @@ -44,20 +43,13 @@ public function findSiteNodeForNodeAddress( ? VisibilityConstraints::frontend() : VisibilityConstraints::withoutRestrictions() ); + $node = $subgraph->findNodeById($nodeAddress->nodeAggregateId); - if (is_null($node)) { + if (!$node) { return null; } - $previousNode = null; - do { - if ($this->getNodeType($node)->isOfType(NodeTypeNameFactory::NAME_SITES)) { - // the Site node is the one level underneath the "Sites" node. - return $previousNode; - } - $previousNode = $node; - } while ($node = $subgraph->findParentNode($node->nodeAggregateId)); - - // no Site node found at rootline - return null; + $siteNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE)); + + return $siteNode; } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php index b24f43096c1..9f5e7b9a4ff 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php +++ b/Neos.Neos/Classes/Domain/Service/SiteNodeUtility.php @@ -16,14 +16,12 @@ namespace Neos\Neos\Domain\Service; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; -use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; @@ -32,70 +30,69 @@ final class SiteNodeUtility { use NodeTypeWithFallbackProvider; - #[Flow\Inject] - protected ContentRepositoryRegistry $contentRepositoryRegistry; - public function __construct( - private readonly DomainRepository $domainRepository, - private readonly SiteRepository $siteRepository + private readonly ContentRepositoryRegistry $contentRepositoryRegistry ) { } - public function findSiteNode(Node $node): Node - { - $previousNode = null; - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); - do { - if ($this->getNodeType($node)->isOfType(NodeTypeNameFactory::NAME_SITES)) { - // the Site node is the one level underneath the "Sites" node. - if (is_null($previousNode)) { - break; - } - return $previousNode; - } - $previousNode = $node; - } while ($node = $subgraph->findParentNode($node->nodeAggregateId)); - - // no Site node found at rootline - throw new \RuntimeException('No site node found!'); - } - - public function findCurrentSiteNode( - ContentRepositoryId $contentRepositoryId, + /** + * Find the site node by the neos site entity. + * + * To find the site node for the live workspace in a 0 dimensional content repository use: + * + * ```php + * $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); + * $liveWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::forLive()) + * ?? throw new \RuntimeException('Expected live workspace to exist.'); + * + * $siteNode = $this->siteNodeUtility->findSiteNodeBySite( + * $site, + * $liveWorkspace->currentContentStreamId, + * DimensionSpacePoint::fromArray([]), + * VisibilityConstraints::frontend() + * ); + * ``` + * + * To resolve the Site by a node use {@see SiteRepository::findSiteBySiteNode()} + */ + public function findSiteNodeBySite( + Site $site, ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, VisibilityConstraints $visibilityConstraints ): Node { - $domain = $this->domainRepository->findOneByActiveRequest(); - $site = $domain - ? $domain->getSite() - : $this->siteRepository->findDefault(); + $contentRepository = $this->contentRepositoryRegistry->get($site->getConfiguration()->contentRepositoryId); - if ($site instanceof Site) { - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $subgraph = $contentRepository->getContentGraph()->getSubgraph( - $contentStreamId, - $dimensionSpacePoint, - $visibilityConstraints, - ); + $subgraph = $contentRepository->getContentGraph()->getSubgraph( + $contentStreamId, + $dimensionSpacePoint, + $visibilityConstraints, + ); + + $rootNodeAggregate = $contentRepository->getContentGraph()->findRootNodeAggregateByType( + $contentStreamId, + NodeTypeNameFactory::forSites() + ); + $rootNode = $rootNodeAggregate->getNodeByCoveredDimensionSpacePoint($dimensionSpacePoint); + + $siteNode = $subgraph->findChildNodeConnectedThroughEdgeName( + $rootNode->nodeAggregateId, + $site->getNodeName()->toNodeName() + ); + + if (!$siteNode) { + throw new \RuntimeException(sprintf('No site node found for site "%s"', $site->getNodeName()), 1697140379); + } - $rootNodeAggregate = $contentRepository->getContentGraph() - ->findRootNodeAggregateByType( - $contentStreamId, - NodeTypeNameFactory::forSites() - ); - $sitesNode = $subgraph->findNodeById($rootNodeAggregate->nodeAggregateId); - if ($sitesNode) { - $siteNode = $subgraph->findChildNodeConnectedThroughEdgeName( - $sitesNode->nodeAggregateId, - $site->getNodeName()->toNodeName() - ); - if ($siteNode instanceof Node) { - return $siteNode; - } - } + if (!$this->getNodeType($siteNode)->isOfType(NodeTypeNameFactory::NAME_SITE)) { + throw new \RuntimeException(sprintf( + 'The site node "%s" (type: "%s") must be of type "%s"', + $siteNode->nodeAggregateId->value, + $siteNode->nodeTypeName->value, + NodeTypeNameFactory::NAME_SITE + ), 1697140367); } - throw new \RuntimeException('No site node found for domain "' . $domain?->getHostname() . '" and site "' . $site?->getNodeName() . '"'); + return $siteNode; } } diff --git a/Neos.Neos/Classes/Domain/Service/SiteService.php b/Neos.Neos/Classes/Domain/Service/SiteService.php index 0331b8be881..c1fafe378bf 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteService.php +++ b/Neos.Neos/Classes/Domain/Service/SiteService.php @@ -14,11 +14,10 @@ namespace Neos\Neos\Domain\Service; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; -use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepository\Core\Factory\ContentRepositoryId; use Neos\Flow\Annotations as Flow; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Media\Domain\Model\Asset; @@ -65,12 +64,6 @@ class SiteService */ protected $assetCollectionRepository; - #[Flow\Inject] - protected SiteNodeUtility $siteNodeUtility; - - #[Flow\Inject] - protected UserService $domainUserService; - /** * Remove given site all nodes for that site and all domains associated. */ @@ -121,12 +114,12 @@ public function pruneAll() */ public function assignUploadedAssetToSiteAssetCollection(Asset $asset, Node $node, string $propertyName) { - try { - $siteNode = $this->siteNodeUtility->findSiteNode($node); - } catch (\InvalidArgumentException $exception) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($node); + $siteNode = $subgraph->findClosestNode($node->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE)); + if (!$siteNode) { + // should not happen return; } - if ($siteNode->nodeName === null) { return; } diff --git a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php index 7c4ee09b84c..77c66be3809 100644 --- a/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php +++ b/Neos.Neos/Classes/Domain/Service/SiteServiceInternals.php @@ -15,32 +15,22 @@ namespace Neos\Neos\Domain\Service; use Neos\ContentRepository\Core\ContentRepository; -use Neos\ContentRepository\Core\DimensionSpace\ContentDimensionZookeeper; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph; +use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; -use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; -use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; +use Neos\ContentRepository\Core\Feature\NodeModification\Dto\PropertyValuesToWrite; use Neos\ContentRepository\Core\Feature\NodeRemoval\Command\RemoveNodeAggregate; use Neos\ContentRepository\Core\Feature\NodeVariation\Command\CreateNodeVariant; -use Neos\ContentRepository\Core\Feature\RootNodeCreation\Command\CreateRootNodeAggregateWithNode; -use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Command\CreateRootWorkspace; +use Neos\ContentRepository\Core\NodeType\NodeTypeManager; +use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; -use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\Service\ContentRepositoryBootstrapper; +use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; -use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; -use Neos\ContentRepository\Core\NodeType\NodeTypeName; -use Neos\ContentRepository\Core\SharedModel\User\UserId; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceDescription; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceTitle; -use Neos\Neos\Domain\Exception\LiveWorkspaceIsMissing; -use Neos\Neos\Domain\Exception\SitesNodeIsMissing; +use Neos\ContentRepository\Core\SharedModel\Node\NodeVariantSelectionStrategy; +use Neos\Neos\Domain\Exception\SiteNodeTypeIsInvalid; use Neos\Neos\Domain\Model\Site; use Neos\Neos\Domain\Model\SiteNodeName; @@ -98,7 +88,7 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi NodeTypeNameFactory::forSites() ); try { - $this->nodeTypeManager->getNodeType($nodeTypeName); + $siteNodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); } catch (NodeTypeNotFoundException $exception) { throw new NodeTypeNotFoundException( 'Cannot create a site using a non-existing node type.', @@ -107,6 +97,10 @@ public function createSiteNodeIfNotExists(Site $site, string $nodeTypeName): voi ); } + if (!$siteNodeType->isOfType(NodeTypeNameFactory::NAME_SITE)) { + throw SiteNodeTypeIsInvalid::becauseItIsNotOfTypeSite(NodeTypeName::fromString($nodeTypeName)); + } + $siteNodeAggregate = $this->contentRepository->getContentGraph()->findChildNodeAggregatesByName( $liveContentStreamId, $sitesNodeIdentifier, diff --git a/Neos.Neos/Classes/Fusion/Helper/BackendHelper.php b/Neos.Neos/Classes/Fusion/Helper/BackendHelper.php index 53c5bd94779..b3dcf1aa0a2 100644 --- a/Neos.Neos/Classes/Fusion/Helper/BackendHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/BackendHelper.php @@ -35,8 +35,6 @@ class BackendHelper implements ProtectedContextAwareInterface */ public function interfaceLanguage(): string { - $currentUser = $this->userService->getBackendUser(); - assert($currentUser !== null, "No backend user"); return $this->userService->getInterfaceLanguage(); } diff --git a/Neos.Neos/Classes/Fusion/Helper/SiteHelper.php b/Neos.Neos/Classes/Fusion/Helper/SiteHelper.php index 05e8a283d5f..c00989affe8 100644 --- a/Neos.Neos/Classes/Fusion/Helper/SiteHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/SiteHelper.php @@ -14,16 +14,11 @@ namespace Neos\Neos\Fusion\Helper; -use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\Neos\Domain\Model\Site; -use Neos\Neos\Domain\Model\SiteNodeName; -use Neos\Neos\Domain\Repository\SiteRepository; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\Flow\Annotations as Flow; use Neos\Eel\ProtectedContextAwareInterface; -use Neos\Neos\Domain\Exception; +use Neos\Flow\Annotations as Flow; +use Neos\Neos\Domain\Model\Site; +use Neos\Neos\Domain\Repository\SiteRepository; /** * Eel helper for accessing the Site object @@ -36,17 +31,13 @@ class SiteHelper implements ProtectedContextAwareInterface */ protected $siteRepository; - /** - * - * @throws Exception - */ public function findBySiteNode(Node $siteNode): ?Site { - if ($siteNode->nodeName === null) { + try { + return $this->siteRepository->findSiteBySiteNode($siteNode); + } catch (\Neos\Neos\Domain\Exception) { return null; } - $siteNodeName = SiteNodeName::fromNodeName($siteNode->nodeName); - return $this->siteRepository->findOneByNodeName($siteNodeName); } /** diff --git a/Neos.Neos/Classes/Service/BackendRedirectionService.php b/Neos.Neos/Classes/Service/BackendRedirectionService.php index 0b8072ba1c0..dd321794071 100644 --- a/Neos.Neos/Classes/Service/BackendRedirectionService.php +++ b/Neos.Neos/Classes/Service/BackendRedirectionService.php @@ -136,54 +136,4 @@ protected function determineStartModule(array $availableModules): ?array return $firstModule; } - - /** - * Returns a specific URI string to redirect to after the logout; or NULL if there is none. - * In case of NULL, it's the responsibility of the AuthenticationController where to redirect, - * most likely to the LoginController's index action. - * - * @param ActionRequest $actionRequest - * @return string A possible redirection URI, if any - * @throws \Neos\Flow\Http\Exception - * @throws MissingActionNameException - */ - public function getAfterLogoutRedirectionUri(ActionRequest $actionRequest): ?string - { - $lastVisitedNode = $this->getLastVisitedNode('live', $actionRequest); - if ($lastVisitedNode === null) { - return null; - } - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($actionRequest); - $uriBuilder->setFormat('html'); - $uriBuilder->setCreateAbsoluteUri(true); - - return $uriBuilder->uriFor('show', ['node' => $lastVisitedNode], 'Frontend\\Node', 'Neos.Neos'); - } - - protected function getLastVisitedNode(string $workspaceName, ActionRequest $actionRequest): ?Node - { - $contentRepositoryId = SiteDetectionResult::fromRequest($actionRequest->getHttpRequest()) - ->contentRepositoryId; - $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspaceName)); - if (!$workspace || !$this->session->isStarted() || !$this->session->hasKey('lastVisitedNode')) { - return null; - } - try { - /** @var Node $lastVisitedNode */ - $lastVisitedNode = $this->propertyMapper->convert( - $this->session->getData('lastVisitedNode'), - Node::class - ); - - return $contentRepository->getContentGraph()->getSubgraph( - $workspace->currentContentStreamId, - $lastVisitedNode->subgraphIdentity->dimensionSpacePoint, - VisibilityConstraints::withoutRestrictions() - )->findNodeById($lastVisitedNode->nodeAggregateId); - } catch (\Exception $exception) { - return null; - } - } } diff --git a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php index e626cfd37a8..b48f920c261 100644 --- a/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php +++ b/Neos.Neos/Classes/Service/NodeTypeSchemaBuilder.php @@ -17,6 +17,7 @@ use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface; use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeType; +use Neos\ContentRepository\Core\SharedModel\Node\NodeName; /** * Renders the Node Type Schema in a format the User Interface understands; @@ -104,9 +105,9 @@ protected function generateConstraints() } } - foreach ($nodeType->getAutoCreatedChildNodes() as $key => $_x) { + foreach ($this->nodeTypeManager->getTetheredNodesConfigurationForNodeType($nodeType) as $key => $_x) { foreach ($nodeTypes as $innerNodeTypeName => $innerNodeType) { - if ($nodeType->allowsGrandchildNodeType($key, $innerNodeType)) { + if ($this->nodeTypeManager->isNodeTypeAllowedAsChildToTetheredNode($nodeType, NodeName::fromString($key), $innerNodeType)) { $constraints[$nodeTypeName]['childNodes'][$key]['nodeTypes'][$innerNodeTypeName] = true; } } diff --git a/Neos.Neos/Classes/View/FusionExceptionView.php b/Neos.Neos/Classes/View/FusionExceptionView.php index 6910e8ffe6b..4406031ca8c 100644 --- a/Neos.Neos/Classes/View/FusionExceptionView.php +++ b/Neos.Neos/Classes/View/FusionExceptionView.php @@ -14,10 +14,8 @@ namespace Neos\Neos\View; -use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; -use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; @@ -36,10 +34,10 @@ use Neos\Fusion\Core\RuntimeFactory; use Neos\Fusion\Exception\RuntimeException; use Neos\Neos\Domain\Model\RenderingMode; +use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; use Neos\Neos\Domain\Service\SiteNodeUtility; -use Neos\Neos\Domain\Service\RenderingModeService; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; class FusionExceptionView extends AbstractView @@ -90,7 +88,7 @@ class FusionExceptionView extends AbstractView protected ContentRepositoryRegistry $contentRepositoryRegistry; #[Flow\Inject] - protected RenderingModeService $userInterfaceModeService; + protected DomainRepository $domainRepository; /** * @return string @@ -117,14 +115,14 @@ public function render() ); $dimensionSpacePoint = $fusionExceptionViewInternals->getArbitraryDimensionSpacePoint(); - $contentStreamId = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::forLive()) - ?->currentContentStreamId; + $liveWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::forLive()); $currentSiteNode = null; - if ($contentStreamId instanceof ContentStreamId) { - $currentSiteNode = $this->siteNodeUtility->findCurrentSiteNode( - $siteDetectionResult->contentRepositoryId, - $contentStreamId, + $site = $this->siteRepository->findOneByNodeName($siteDetectionResult->siteNodeName); + if ($liveWorkspace && $site) { + $currentSiteNode = $this->siteNodeUtility->findSiteNodeBySite( + $site, + $liveWorkspace->currentContentStreamId, $dimensionSpacePoint, VisibilityConstraints::frontend() ); @@ -163,13 +161,12 @@ public function render() try { $output = $fusionRuntime->render('error'); - $output = $this->extractBodyFromOutput($output); + return $this->extractBodyFromOutput($output); } catch (RuntimeException $exception) { throw $exception->getPrevious() ?: $exception; + } finally { + $fusionRuntime->popContext(); } - $fusionRuntime->popContext(); - - return $output; } return ''; diff --git a/Neos.Neos/Classes/View/FusionView.php b/Neos.Neos/Classes/View/FusionView.php index 6fd019f4ad3..c507576e069 100644 --- a/Neos.Neos/Classes/View/FusionView.php +++ b/Neos.Neos/Classes/View/FusionView.php @@ -29,7 +29,6 @@ use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\FusionService; use Neos\Neos\Domain\Service\NodeTypeNameFactory; -use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\Domain\Service\RenderingModeService; use Neos\Neos\Exception; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; @@ -46,12 +45,6 @@ class FusionView extends AbstractView #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** - * @Flow\Inject - * @var SiteNodeUtility - */ - protected $siteNodeUtility; - #[Flow\Inject] protected RuntimeFactory $runtimeFactory; @@ -72,7 +65,13 @@ public function render(): string|ResponseInterface { $currentNode = $this->getCurrentNode(); - $currentSiteNode = $this->siteNodeUtility->findSiteNode($currentNode); + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($currentNode); + $currentSiteNode = $subgraph->findClosestNode($currentNode->nodeAggregateId, FindClosestNodeFilter::create(nodeTypeConstraints: NodeTypeNameFactory::NAME_SITE)); + + if (!$currentSiteNode) { + throw new \RuntimeException('No site node found!', 1697053346); + } + $fusionRuntime = $this->getFusionRuntime($currentSiteNode); $fusionRuntime->pushContextArray([ diff --git a/Neos.Neos/NodeTypes/Mixin/Document.yaml b/Neos.Neos/NodeTypes/Mixin/Document.yaml index 41d37e48cea..f00b1f0dd85 100644 --- a/Neos.Neos/NodeTypes/Mixin/Document.yaml +++ b/Neos.Neos/NodeTypes/Mixin/Document.yaml @@ -11,6 +11,8 @@ constraints: nodeTypes: '*': false + # explicitly disallow to create a homepage below a regular document + 'Neos.Neos:Site': false 'Neos.Neos:Document': true postprocessors: 'CreationDialogPostprocessor': diff --git a/Neos.Neos/NodeTypes/Mixin/Site.yaml b/Neos.Neos/NodeTypes/Mixin/Site.yaml new file mode 100644 index 00000000000..c2d38ef77a3 --- /dev/null +++ b/Neos.Neos/NodeTypes/Mixin/Site.yaml @@ -0,0 +1,10 @@ +# +# Abstract NodeType for a Neos.Neos site. +# Each homepage must extend this NodeType. +# Nodes of this type must be direct children of the Neos.Neos:Site Root +# and must not be created at any other place in the tree. +# +'Neos.Neos:Site': + abstract: true + superTypes: + 'Neos.Neos:Document': true diff --git a/Neos.Neos/NodeTypes/Root/Sites.yaml b/Neos.Neos/NodeTypes/Root/Sites.yaml index fa2556e5904..229957b03dd 100644 --- a/Neos.Neos/NodeTypes/Root/Sites.yaml +++ b/Neos.Neos/NodeTypes/Root/Sites.yaml @@ -1,3 +1,11 @@ +# +# Root NodeType for a Neos.Neos content repository. +# Any Neos Site node must be its direct child. +# 'Neos.Neos:Sites': superTypes: 'Neos.ContentRepository:Root': true + constraints: + nodeTypes: + '*': false + 'Neos.Neos:Site': true diff --git a/Neos.Neos/Resources/Private/Fusion/Backend/Views/Login.fusion b/Neos.Neos/Resources/Private/Fusion/Backend/Views/Login.fusion index 7d9fc5288d7..7c8d824fc46 100644 --- a/Neos.Neos/Resources/Private/Fusion/Backend/Views/Login.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Backend/Views/Login.fusion @@ -88,9 +88,6 @@ prototype(Neos.Neos:View.Login) < prototype(Neos.Fusion:Component) { document.querySelector('.neos-login-btn').classList.toggle('neos-hidden'); document.querySelector('.neos-login-btn.neos-disabled').classList.toggle('neos-hidden'); }); - try { - document.querySelector('form[name=\"login\"] input[name=\"lastVisitedNode\"]').value = sessionStorage.getItem('Neos.Neos.lastVisitedNode'); - } catch(e) {} "} @@ -111,7 +108,6 @@ prototype(Neos.Neos:Component.Login.Form) < prototype(Neos.Fusion:Component) { renderer = afx` -
+

{String.htmlspecialchars(props.exception.headingMessagePart)}

@@ -24,6 +24,6 @@ prototype(Neos.Neos:Error.View.FusionParserException) < prototype(Neos.Fusion:Co
- +
` } diff --git a/Neos.Neos/Resources/Private/Fusion/ErrorCase.fusion b/Neos.Neos/Resources/Private/Fusion/ErrorCase.fusion index b5bc4aa1111..83f191b2043 100644 --- a/Neos.Neos/Resources/Private/Fusion/ErrorCase.fusion +++ b/Neos.Neos/Resources/Private/Fusion/ErrorCase.fusion @@ -10,10 +10,7 @@ error = Neos.Fusion:Case { default { @position = 'end 9999' condition = true - renderer = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos/Private/Templates/Error/Index.html' - layoutRootPath = 'resource://Neos.Neos/Private/Layouts/' - + renderer = Neos.Neos:DefaultExceptionRenderer { exception = ${exception} renderingOptions = ${renderingOptions} statusCode = ${statusCode} @@ -22,3 +19,29 @@ error = Neos.Fusion:Case { } } } + +/** @internal */ +prototype(Neos.Neos:DefaultExceptionRenderer) < prototype(Neos.Fusion:Component) { + /** @var exception \Exception */ + exception = null + renderingOptions = null + + renderer = afx` + + +
+
+
+ +
+

{Translation.id('error.exception.' + props.renderingOptions.renderingGroup + '.title').package('Neos.Neos').locale(Neos.Backend.interfaceLanguage()).translate()}

+
+

+ {String.nl2br(Translation.id('error.exception.' + props.renderingOptions.renderingGroup + '.description').package('Neos.Neos').locale(Neos.Backend.interfaceLanguage()).translate())} +

+

#{props.exception.code}: {props.exception.message}

+
+ +
+ ` +} diff --git a/Neos.Neos/Resources/Private/Fusion/Error/Views/Page.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/ErrorPage.fusion similarity index 90% rename from Neos.Neos/Resources/Private/Fusion/Error/Views/Page.fusion rename to Neos.Neos/Resources/Private/Fusion/Prototypes/ErrorPage.fusion index 58c2788735c..bfeaf3e6924 100644 --- a/Neos.Neos/Resources/Private/Fusion/Error/Views/Page.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/ErrorPage.fusion @@ -1,4 +1,8 @@ -prototype(Neos.Neos:Error.View.Page) < prototype(Neos.Fusion:Component) { +/** + * Only for internal use! + * @internal + */ +prototype(Neos.Neos:ErrorPage) < prototype(Neos.Fusion:Component) { title = '' content = '' diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/FallbackNode.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/FallbackNode.fusion index f82b10bca43..571d14fdc7a 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/FallbackNode.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/FallbackNode.fusion @@ -1,4 +1,15 @@ -prototype(Neos.Neos:FallbackNode) < prototype(Neos.Neos:Content) { - templatePath = 'resource://Neos.Neos/Private/Templates/FusionObjects/FallbackNode.html' +prototype(Neos.Neos:FallbackNode) < prototype(Neos.Neos:ContentComponent) { @if.onlyRenderInBackend = ${renderingMode.isEdit || renderingMode.isPreview} + + renderer = afx` +
+
+
+

{Translation.id('error.invalidNodeType.title').package('Neos.Neos').locale(Neos.Backend.interfaceLanguage()).translate()}

+
+
+

{Translation.id('error.invalidNodeType.description').package('Neos.Neos').locale(Neos.Backend.interfaceLanguage()).translate()}

+
+
+ ` } diff --git a/Neos.Neos/Resources/Private/Fusion/Prototypes/Page.fusion b/Neos.Neos/Resources/Private/Fusion/Prototypes/Page.fusion index 59216dc14a1..b0cdf9685c0 100644 --- a/Neos.Neos/Resources/Private/Fusion/Prototypes/Page.fusion +++ b/Neos.Neos/Resources/Private/Fusion/Prototypes/Page.fusion @@ -79,20 +79,6 @@ prototype(Neos.Neos:Page) < prototype(Neos.Fusion:Http.Message) { @process.appendJavaScripts = ${value + this.javascripts} } - # This enables redirecting to the last visited page after login - lastVisitedNodeScript = Neos.Fusion:Tag { - @position = 'before closingBodyTag' - - tagName = 'script' - attributes { - data-neos-node = ${Neos.Node.serializedNodeAddress(node)} - src = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos/Public/JavaScript/LastVisitedNode.js' - } - async = true - } - } - closingBodyTag = '' closingBodyTag.@position = 'end 100' diff --git a/Neos.Neos/Resources/Private/Templates/Error/Index.html b/Neos.Neos/Resources/Private/Templates/Error/Index.html deleted file mode 100755 index b78250efe90..00000000000 --- a/Neos.Neos/Resources/Private/Templates/Error/Index.html +++ /dev/null @@ -1,28 +0,0 @@ -{namespace neos=Neos\Neos\ViewHelpers} - - - - Neos Error - - - - - -
-
-
- -
-

{neos:backend.translate(id: 'error.exception.{renderingOptions.renderingGroup}.title', package: 'Neos.Neos')}

-
-

{neos:backend.translate(id: 'error.exception.{renderingOptions.renderingGroup}.description', package: 'Neos.Neos') -> f:format.nl2br()}

- -

#{exception.code}: {exception.message}

-
- -

{neos:backend.translate(id: 'error.exception.{renderingOptions.renderingGroup}.setupMessage', package: 'Neos.Neos')}

-

{neos:backend.translate(id: 'error.exception.goToSetup', package: 'Neos.Neos')}

-
-
- -
diff --git a/Neos.Neos/Resources/Private/Templates/FusionObjects/FallbackNode.html b/Neos.Neos/Resources/Private/Templates/FusionObjects/FallbackNode.html deleted file mode 100644 index f9d73915fa9..00000000000 --- a/Neos.Neos/Resources/Private/Templates/FusionObjects/FallbackNode.html +++ /dev/null @@ -1,10 +0,0 @@ -{namespace neos=Neos\Neos\ViewHelpers} - f:format.raw()}> -
-
-

{neos:backend.translate(id: 'error.invalidNodeType.title', package: 'Neos.Neos')}

-
-
-

{neos:backend.translate(id: 'error.invalidNodeType.description', package: 'Neos.Neos')}

-
-
diff --git a/Neos.Neos/Resources/Public/JavaScript/LastVisitedNode.js b/Neos.Neos/Resources/Public/JavaScript/LastVisitedNode.js deleted file mode 100644 index 2a00c8f7fa2..00000000000 --- a/Neos.Neos/Resources/Public/JavaScript/LastVisitedNode.js +++ /dev/null @@ -1,8 +0,0 @@ -try { - sessionStorage.setItem( - "Neos.Neos.lastVisitedNode", - document - .querySelector("script[data-neos-node]") - .getAttribute("data-neos-node") - ); -} catch (e) {} diff --git a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php index b0c1d52fafa..5d5723b2bec 100644 --- a/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php +++ b/Neos.Neos/Tests/Unit/NodeTypePostprocessor/DefaultPropertyEditorPostprocessorTest.php @@ -14,7 +14,6 @@ use Neos\ContentRepository\Core\NodeType\DefaultNodeLabelGeneratorFactory; use Neos\ContentRepository\Core\NodeType\NodeType; -use Neos\ContentRepository\Core\NodeType\NodeTypeManager; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\Flow\Tests\UnitTestCase; use Neos\Neos\NodeTypePostprocessor\DefaultPropertyEditorPostprocessor; @@ -33,10 +32,6 @@ private function processConfiguration(array $configuration, array $dataTypesDefa NodeTypeName::fromString('Some.NodeType:Name'), [], [], - new NodeTypeManager( - fn () => [], - new DefaultNodeLabelGeneratorFactory() - ), new DefaultNodeLabelGeneratorFactory() ); $postprocessor->process($mockNodeType, $configuration, []);