diff --git a/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php b/Neos.ContentRepository.LegacyNodeMigration/Classes/NodeDataToEventsProcessor.php index ba5440ffc16..8d256106bc8 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) 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.Media.Browser/Classes/Controller/UsageController.php b/Neos.Media.Browser/Classes/Controller/UsageController.php index eb3dab88e26..6a1c6b924fe 100644 --- a/Neos.Media.Browser/Classes/Controller/UsageController.php +++ b/Neos.Media.Browser/Classes/Controller/UsageController.php @@ -12,20 +12,21 @@ * source code. */ +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\Eel\FlowQuery\FlowQuery; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Service\AssetService; use Neos\Neos\Domain\Repository\SiteRepository; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Service\UserService; use Neos\Neos\Domain\Service\UserService as DomainUserService; use Neos\Neos\AssetUsage\Dto\AssetUsageReference; -use Neos\Neos\Domain\Service\SiteNodeUtility; use Neos\Neos\Domain\Model\Site; /** @@ -65,12 +66,6 @@ class UsageController extends ActionController */ protected $domainUserService; - /** - * @Flow\Inject - * @var SiteNodeUtility - */ - protected $siteNodeUtility; - /** * Get Related Nodes for an asset * @@ -126,28 +121,32 @@ public function relatedNodesAction(AssetInterface $asset) continue; } - $node = $contentRepository->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/Module/Administration/SitesController.php b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php index 75d8f5b4a9c..04fecdde89d 100755 --- a/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php +++ b/Neos.Neos/Classes/Controller/Module/Administration/SitesController.php @@ -398,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, diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index 34ba8e99ea5..682c71b212e 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -798,7 +798,8 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos if (is_null($documentNode)) { $documentNode = $ancestor; } - // the site node is the last ancestor of type Document + } + 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/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/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