diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php index 31532f5e148..4858d720f8d 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php @@ -88,11 +88,6 @@ final public static function fromLegacyDimensionArray(array $legacyDimensionValu return self::instance($coordinates); } - final public static function fromUriRepresentation(string $encoded): self - { - return self::instance(json_decode(base64_decode($encoded), true)); - } - /** * Varies a dimension space point in a single coordinate */ diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php index 4025758bd8d..fcce795b58c 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php @@ -65,16 +65,21 @@ public static function fromNode(Node $node): self public static function fromArray(array $array): self { return new self( - ContentRepositoryId::fromString($array['contentRepositoryId']), - WorkspaceName::fromString($array['workspaceName']), - DimensionSpacePoint::fromArray($array['dimensionSpacePoint']), - NodeAggregateId::fromString($array['aggregateId']) + ContentRepositoryId::fromString($array['contentRepositoryId'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "contentRepositoryId" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478573)), + WorkspaceName::fromString($array['workspaceName'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "workspaceName" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478580)), + DimensionSpacePoint::fromArray($array['dimensionSpacePoint'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "dimensionSpacePoint" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478584)), + NodeAggregateId::fromString($array['aggregateId'] ?? throw new \InvalidArgumentException(sprintf('Failed to decode NodeAddress from array. Key "aggregateId" does not exist. Got: %s', json_encode($array, JSON_PARTIAL_OUTPUT_ON_ERROR)), 1716478588)) ); } public static function fromJsonString(string $jsonString): self { - return self::fromArray(\json_decode($jsonString, true, JSON_THROW_ON_ERROR)); + try { + $jsonArray = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new \InvalidArgumentException(sprintf('Failed to JSON-decode NodeAddress: %s', $e->getMessage()), 1716478364, $e); + } + return self::fromArray($jsonArray); } public function withAggregateId(NodeAggregateId $aggregateId): self diff --git a/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php b/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php new file mode 100644 index 00000000000..9c7de0cbade --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php @@ -0,0 +1,78 @@ + [ + 'nodeAddress' => NodeAddress::create( + ContentRepositoryId::fromString('default'), + WorkspaceName::forLive(), + DimensionSpacePoint::createWithoutDimensions(), + NodeAggregateId::fromString('marcus-heinrichus') + ), + 'serialized' => '{"contentRepositoryId":"default","workspaceName":"live","dimensionSpacePoint":[],"aggregateId":"marcus-heinrichus"}' + ]; + + yield 'one dimension' => [ + 'nodeAddress' => NodeAddress::create( + ContentRepositoryId::fromString('default'), + WorkspaceName::fromString('user-mh'), + DimensionSpacePoint::fromArray(['language' => 'de']), + NodeAggregateId::fromString('79e69d1c-b079-4535-8c8a-37e76736c445') + ), + 'serialized' => '{"contentRepositoryId":"default","workspaceName":"user-mh","dimensionSpacePoint":{"language":"de"},"aggregateId":"79e69d1c-b079-4535-8c8a-37e76736c445"}' + ]; + + yield 'two dimensions' => [ + 'nodeAddress' => NodeAddress::create( + ContentRepositoryId::fromString('second'), + WorkspaceName::fromString('user-mh'), + DimensionSpacePoint::fromArray(['language' => 'en_US', 'audience' => 'nice people']), + NodeAggregateId::fromString('my-node-id') + ), + 'serialized' => '{"contentRepositoryId":"second","workspaceName":"user-mh","dimensionSpacePoint":{"language":"en_US","audience":"nice people"},"aggregateId":"my-node-id"}' + ]; + } + + /** + * @dataProvider jsonSerialization + * @test + */ + public function serialization(NodeAddress $nodeAddress, string $expected): void + { + self::assertEquals($expected, $nodeAddress->toJson()); + } + + /** + * @dataProvider jsonSerialization + * @test + */ + public function deserialization(NodeAddress $expectedNodeAddress, string $encoded): void + { + $nodeAddress = NodeAddress::fromJsonString($encoded); + self::assertInstanceOf(NodeAddress::class, $nodeAddress); + self::assertTrue($expectedNodeAddress->equals($nodeAddress)); + } +} diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 4fc24f62bf9..0f2cbf16f38 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -14,7 +14,6 @@ namespace Neos\Neos\Controller\Frontend; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\ContentSubgraphWithRuntimeCaches; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphWithRuntimeCaches\InMemoryCache; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -25,6 +24,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Subtree; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; @@ -39,10 +39,8 @@ use Neos\Neos\Domain\Service\RenderingModeService; use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Neos\View\FusionView; @@ -106,6 +104,9 @@ class NodeController extends ActionController #[Flow\InjectConfiguration(path: "frontend.shortcutRedirectHttpStatusCode", package: "Neos.Neos")] protected int $shortcutRedirectHttpStatusCode; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; + /** * @param string $node * @throws NodeNotFoundException @@ -130,21 +131,14 @@ public function previewAction(string $node): void $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); - $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node); + $nodeAddress = NodeAddress::fromJsonString($node); $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, $visibilityConstraints ); - $site = $subgraph->findClosestNode($nodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); - if ($site === null) { - throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for address " . $nodeAddress); - } - - $this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph); - - $nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId); + $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); if (is_null($nodeInstance)) { throw new NodeNotFoundException( @@ -153,12 +147,19 @@ public function previewAction(string $node): void ); } + $site = $subgraph->findClosestNode($nodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + if ($site === null) { + throw new NodeNotFoundException("TODO: SITE NOT FOUND; should not happen (for identity " . $nodeAddress->toJson()); + } + + $this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph); + if ( $this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT) && !$renderingMode->isEdit && $nodeAddress->workspaceName->isLive() // shortcuts are only resolvable for the live workspace ) { - $this->handleShortcutNode($nodeAddress, $contentRepository); + $this->handleShortcutNode($nodeAddress); } $this->view->setOption('renderingModeName', $renderingMode->name); @@ -192,33 +193,33 @@ public function previewAction(string $node): void */ public function showAction(string $node): void { - $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); - $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); + $nodeAddress = NodeAddress::fromJsonString($node); + unset($node); - $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node); - if (!$nodeAddress->isInLiveWorkspace()) { + if (!$nodeAddress->workspaceName->isLive()) { throw new NodeNotFoundException('The requested node isn\'t accessible to the current user', 1430218623); } + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, VisibilityConstraints::frontend() ); - $nodeInstance = $subgraph->findNodeById($nodeAddress->nodeAggregateId); + $nodeInstance = $subgraph->findNodeById($nodeAddress->aggregateId); if ($nodeInstance === null) { - throw new NodeNotFoundException(sprintf('The cached node address for this uri could not be resolved. Possibly you have to flush the "Flow_Mvc_Routing_Route" cache. %s', $nodeAddress), 1707300738); + throw new NodeNotFoundException(sprintf('The cached node address for this uri could not be resolved. Possibly you have to flush the "Flow_Mvc_Routing_Route" cache. %s', $nodeAddress->toJson()), 1707300738); } - $site = $subgraph->findClosestNode($nodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + $site = $subgraph->findClosestNode($nodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); if ($site === null) { - throw new NodeNotFoundException(sprintf('The site node of %s could not be resolved.', $nodeAddress), 1707300861); + throw new NodeNotFoundException(sprintf('The site node of %s could not be resolved.', $nodeAddress->toJson()), 1707300861); } - $this->fillCacheWithContentNodes($nodeAddress->nodeAggregateId, $subgraph); + $this->fillCacheWithContentNodes($nodeAddress->aggregateId, $subgraph); if ($this->getNodeType($nodeInstance)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) { - $this->handleShortcutNode($nodeAddress, $contentRepository); + $this->handleShortcutNode($nodeAddress); } $this->view->setOption('renderingModeName', RenderingMode::FRONTEND); @@ -266,31 +267,31 @@ protected function overrideViewVariablesFromInternalArguments() /** * Handles redirects to shortcut targets of nodes in the live workspace. * - * @param NodeAddress $nodeAddress * @throws NodeNotFoundException * @throws \Neos\Flow\Mvc\Exception\StopActionException */ - protected function handleShortcutNode(NodeAddress $nodeAddress, ContentRepository $contentRepository): void + protected function handleShortcutNode(NodeAddress $nodeAddress): void { try { - $resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress, $contentRepository); + $resolvedTarget = $this->nodeShortcutResolver->resolveShortcutTarget($nodeAddress); } catch (InvalidShortcutException $e) { throw new NodeNotFoundException(sprintf( - 'The shortcut node target of node "%s" could not be resolved: %s', - $nodeAddress, + 'The shortcut node target of node %s could not be resolved: %s', + $nodeAddress->toJson(), $e->getMessage() ), 1430218730, $e); } if ($resolvedTarget instanceof NodeAddress) { - if ($resolvedTarget === $nodeAddress) { + if ($nodeAddress->equals($resolvedTarget)) { return; } try { - $resolvedUri = NodeUriBuilder::fromRequest($this->request)->uriFor($nodeAddress); + $resolvedUri = $this->nodeUriBuilderFactory->forActionRequest($this->request) + ->uriFor($nodeAddress); } catch (NoMatchingRouteException $e) { throw new NodeNotFoundException(sprintf( - 'The shortcut node target of node "%s" could not be resolved: %s', - $nodeAddress, + 'The shortcut node target of node %s could not be resolved: %s', + $nodeAddress->toJson(), $e->getMessage() ), 1599670695, $e); } diff --git a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php index a149cdb6f4b..ae240471fae 100644 --- a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php +++ b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php @@ -16,6 +16,7 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; @@ -27,6 +28,7 @@ use Neos\Flow\Mvc\Routing\DynamicRoutePartInterface; use Neos\Flow\Mvc\Routing\ParameterAwareRoutePartInterface; use Neos\Flow\Mvc\Routing\RoutingMiddleware; +use Neos\Neos\Domain\Model\SiteNodeName; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\FrontendRouting\CrossSiteLinking\CrossSiteLinkerInterface; use Neos\Neos\FrontendRouting\DimensionResolution\DelegatingResolver; @@ -201,7 +203,7 @@ public function matchWithParameters(&$requestPath, RouteParameters $parameters) // TODO validate dsp == complete (ContentDimensionZookeeper::getAllowedDimensionSubspace()->contains()...) // if incomplete -> no match + log - $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); + $contentRepository = $this->contentRepositoryRegistry->get($resolvedSite->getConfiguration()->contentRepositoryId); try { $matchResult = $this->matchUriPath( @@ -240,12 +242,13 @@ private function matchUriPath( $uriPath, $dimensionSpacePoint->hash ); - $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId( - $documentUriPathFinder->getLiveContentStreamId(), + $nodeAddress = NodeAddress::create( + $contentRepository->id, + WorkspaceName::forLive(), $dimensionSpacePoint, $nodeInfo->getNodeAggregateId(), ); - return new MatchResult($nodeAddress->serializeForUri(), $nodeInfo->getRouteTags()); + return new MatchResult($nodeAddress->toJson(), $nodeInfo->getRouteTags()); } /** @@ -261,15 +264,14 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para $currentRequestSiteDetectionResult = SiteDetectionResult::fromRouteParameters($parameters); $nodeAddress = $routeValues[$this->name]; - // TODO: for cross-CR links: NodeAddressInContentRepository as a new value object if (!$nodeAddress instanceof NodeAddress) { return false; } try { - $resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult); - } catch (NodeNotFoundException | InvalidShortcutException $exception) { - // TODO log exception + $resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName); + } catch (NodeNotFoundException | TargetSiteNotFoundException | InvalidShortcutException $exception) { + // TODO log exception ... yes todo return false; } @@ -284,23 +286,20 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para * To disallow showing a node actually disabled/hidden itself has to be ensured in matching a request path, * not in building one. * - * @param NodeAddress $nodeAddress - * @param SiteDetectionResult $currentRequestSiteDetectionResult - * @return ResolveResult * @throws InvalidShortcutException * @throws NodeNotFoundException + * @throws TargetSiteNotFoundException */ private function resolveNodeAddress( NodeAddress $nodeAddress, - SiteDetectionResult $currentRequestSiteDetectionResult + SiteNodeName $currentRequestSiteNodeName ): ResolveResult { - // TODO: SOMEHOW FIND OTHER CONTENT REPOSITORY HERE FOR CROSS-CR LINKS!! $contentRepository = $this->contentRepositoryRegistry->get( - $currentRequestSiteDetectionResult->contentRepositoryId + $nodeAddress->contentRepositoryId ); $documentUriPathFinder = $contentRepository->projectionState(DocumentUriPathFinder::class); $nodeInfo = $documentUriPathFinder->getByIdAndDimensionSpacePointHash( - $nodeAddress->nodeAggregateId, + $nodeAddress->aggregateId, $nodeAddress->dimensionSpacePoint->hash ); @@ -318,7 +317,7 @@ private function resolveNodeAddress( } $uriConstraints = UriConstraints::create(); - if (!$targetSite->getNodeName()->equals($currentRequestSiteDetectionResult->siteNodeName)) { + if (!$targetSite->getNodeName()->equals($currentRequestSiteNodeName)) { $uriConstraints = $this->crossSiteLinker->applyCrossSiteUriConstraints( $targetSite, $uriConstraints diff --git a/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php b/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php index 40766ae9654..feb754ba03e 100644 --- a/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php +++ b/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php @@ -20,8 +20,6 @@ * Marker interface which can be used to replace the currently used FrontendNodeRoutePartHandler, * to e.g. use the one with localization support. * - * TODO CORE MIGRATION - * * **See {@see EventSourcedFrontendNodeRoutePartHandler} documentation for a * detailed explanation of the Frontend Routing process.** */ diff --git a/Neos.Neos/Classes/FrontendRouting/NodeAddress.php b/Neos.Neos/Classes/FrontendRouting/NodeAddress.php index 1126c00babe..0f71cdaa166 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeAddress.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeAddress.php @@ -15,6 +15,7 @@ namespace Neos\Neos\FrontendRouting; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -49,16 +50,6 @@ public function __construct( ) { } - public function withNodeAggregateId(NodeAggregateId $nodeAggregateId): self - { - return new self( - $this->contentStreamId, - $this->dimensionSpacePoint, - $nodeAggregateId, - $this->workspaceName - ); - } - public function serializeForUri(): string { // the reverse method is {@link NodeAddressFactory::createFromUriString} - ensure to adjust it diff --git a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php index 7d187251677..9bc511fdcfe 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php @@ -15,9 +15,11 @@ namespace Neos\Neos\FrontendRouting; use Neos\ContentRepository\Core\ContentRepository; +use Neos\Neos\FrontendRouting\NodeAddress as LegacyNodeAddress; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -40,7 +42,7 @@ public function createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateI ContentStreamId $contentStreamId, DimensionSpacePoint $dimensionSpacePoint, NodeAggregateId $nodeAggregateId - ): NodeAddress { + ): LegacyNodeAddress { $workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( $contentStreamId ); @@ -51,7 +53,7 @@ public function createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateI . ' is not assigned to a workspace.' ); } - return new NodeAddress( + return new LegacyNodeAddress( $contentStreamId, $dimensionSpacePoint, $nodeAggregateId, @@ -59,7 +61,7 @@ public function createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateI ); } - public function createFromNode(Node $node): NodeAddress + public function createFromNode(Node $node): LegacyNodeAddress { return $this->createFromContentStreamIdAndDimensionSpacePointAndNodeAggregateId( $node->subgraphIdentity->contentStreamId, @@ -68,7 +70,19 @@ public function createFromNode(Node $node): NodeAddress ); } - public function createFromUriString(string $serializedNodeAddress): NodeAddress + public function createCoreNodeAddressFromLegacyUriString(string $serializedNodeAddress): NodeAddress + { + $legacy = $this->createFromUriString($serializedNodeAddress); + + return NodeAddress::create( + $this->contentRepository->id, + $legacy->workspaceName, + $legacy->dimensionSpacePoint, + $legacy->nodeAggregateId + ); + } + + public function createFromUriString(string $serializedNodeAddress): LegacyNodeAddress { // the reverse method is {@link NodeAddress::serializeForUri} - ensure to adjust it // when changing the serialization here @@ -76,7 +90,7 @@ public function createFromUriString(string $serializedNodeAddress): NodeAddress list($workspaceNameSerialized, $dimensionSpacePointSerialized, $nodeAggregateIdSerialized) = explode('__', $serializedNodeAddress); $workspaceName = WorkspaceName::fromString($workspaceNameSerialized); - $dimensionSpacePoint = DimensionSpacePoint::fromUriRepresentation($dimensionSpacePointSerialized); + $dimensionSpacePoint = DimensionSpacePoint::fromArray(json_decode(base64_decode($dimensionSpacePointSerialized), true)); $nodeAggregateId = NodeAggregateId::fromString($nodeAggregateIdSerialized); $contentStreamId = $this->contentRepository->getWorkspaceFinder()->findOneByName($workspaceName) @@ -88,7 +102,7 @@ public function createFromUriString(string $serializedNodeAddress): NodeAddress ); } - return new NodeAddress( + return new LegacyNodeAddress( $contentStreamId, $dimensionSpacePoint, $nodeAggregateId, diff --git a/Neos.Neos/Classes/FrontendRouting/NodeShortcutResolver.php b/Neos.Neos/Classes/FrontendRouting/NodeShortcutResolver.php index 94ad0a81ad4..2f3d563062e 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeShortcutResolver.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeShortcutResolver.php @@ -17,6 +17,8 @@ use GuzzleHttp\Psr7\Uri; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; @@ -36,16 +38,11 @@ */ class NodeShortcutResolver { - private AssetRepository $assetRepository; - - private ResourceManager $resourceManager; - public function __construct( - AssetRepository $assetRepository, - ResourceManager $resourceManager + private readonly AssetRepository $assetRepository, + private readonly ResourceManager $resourceManager, + private readonly ContentRepositoryRegistry $contentRepositoryRegistry ) { - $this->assetRepository = $assetRepository; - $this->resourceManager = $resourceManager; } /** @@ -53,21 +50,21 @@ public function __construct( * Note: The ContentStreamId is not required for this service, * because it is only covering the live workspace * - * @param NodeAddress $nodeAddress - * @return NodeAddress|UriInterface NodeAddress is returned if we want to link to another node + * @return NodeAddress|UriInterface NodeIdentity is returned if we want to link to another node * (i.e. node is NOT a shortcut node; or target is a node); * or UriInterface for links to fixed URLs (Asset URLs or external URLs) * @throws \Neos\Neos\FrontendRouting\Exception\InvalidShortcutException * @throws NodeNotFoundException */ - public function resolveShortcutTarget(NodeAddress $nodeAddress, ContentRepository $contentRepository) + public function resolveShortcutTarget(NodeAddress $nodeAddress) { if (!$nodeAddress->workspaceName->isLive()) { - throw new \RuntimeException(sprintf('Cannot resolve shortcut target for node-address %s in workspace %s because the DocumentUriPathProjection only handles the live workspace.', $nodeAddress->nodeAggregateId->value, $nodeAddress->workspaceName->value), 1707208650); + throw new \RuntimeException(sprintf('Cannot resolve shortcut target for node-address %s in workspace %s because the DocumentUriPathProjection only handles the live workspace.', $nodeAddress->aggregateId->value, $nodeAddress->workspaceName->value), 1707208650); } + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); $documentUriPathFinder = $contentRepository->projectionState(DocumentUriPathFinder::class); $documentNodeInfo = $documentUriPathFinder->getByIdAndDimensionSpacePointHash( - $nodeAddress->nodeAggregateId, + $nodeAddress->aggregateId, $nodeAddress->dimensionSpacePoint->hash ); $resolvedTarget = $this->resolveNode($documentNodeInfo, $contentRepository); @@ -77,7 +74,7 @@ public function resolveShortcutTarget(NodeAddress $nodeAddress, ContentRepositor if ($resolvedTarget === $documentNodeInfo) { return $nodeAddress; } - return $nodeAddress->withNodeAggregateId($documentNodeInfo->getNodeAggregateId()); + return $nodeAddress->withAggregateId($documentNodeInfo->getNodeAggregateId()); } /** diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index 0e7a23493e6..71fbd46e4d0 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -14,80 +14,175 @@ namespace Neos\Neos\FrontendRouting; -use GuzzleHttp\Psr7\Uri; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Mvc\ActionRequest; +use Neos\Flow\Http\Helper\UriHelper; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; +use Neos\Flow\Mvc\Routing\Dto\ResolveContext; +use Neos\Flow\Mvc\Routing\Dto\RouteParameters; +use Neos\Flow\Mvc\Routing\RouterInterface; use Neos\Flow\Mvc\Routing\UriBuilder; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Psr\Http\Message\UriInterface; /** + * Neos abstraction to simplify node uri building. + * * Builds URIs to nodes, taking workspace (live / shared / user) into account. - * This class can also be used in order to render "preview" URLs to nodes - * that are not in the live workspace (in the Neos Backend and shared workspaces) + * Can also be used in order to render "preview" URLs to nodes, that are not + * in the live workspace (in the Neos Backend and shared workspaces) + * + * Internally a Flow route is configured using the {@see EventSourcedFrontendNodeRoutePartHandler}. + * Streamlines the uri building to not having to interact with the {@see UriBuilder} or having to serialize the node address. + * + * @api except its constructor */ -final class NodeUriBuilder +#[Flow\Proxy(false)] +final readonly class NodeUriBuilder { - private UriBuilder $uriBuilder; - /** - * @Flow\Autowiring(false) + * Please inject and use the {@see NodeUriBuilderFactory} to acquire this uri builder + * + * #[Flow\Inject] + * protected NodeUriBuilderFactory $nodeUriBuilderFactory; + * + * $this->nodeUriBuilderFactory->forRequest($someHttpRequest); + * + * @internal must not be manually instantiated but its factory must be used */ - private function __construct(UriBuilder $uriBuilder) - { - $this->uriBuilder = $uriBuilder; - } - - public static function fromRequest(ActionRequest $request): self - { - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return new self($uriBuilder); - } - - public static function fromUriBuilder(UriBuilder $uriBuilder): self - { - return new self($uriBuilder); + public function __construct( + private RouterInterface $router, + /** + * The base uri either set by using Neos.Flow.http.baseUri or inferred from the current request. + * Note that hard coding the base uri in the settings will not work for multi sites and is only to be used as escape hatch for running Neos in a sub-directory + */ + private UriInterface $baseUri, + /** + * This prefix could be used to append to all uris a prefix via `SCRIPT_NAME`, but this feature is currently not well tested and considered experimental + */ + private string $uriPathPrefix, + /** + * The currently active http attributes that are used to influence the routing. The Neos frontend route part handler requires the {@see SiteDetectionResult} to be serialized in here. + */ + private RouteParameters $routeParameters + ) { } /** - * Renders an URI for the given $nodeAddress - * If the node belongs to the live workspace, the public URL is generated - * Otherwise a preview URI is rendered (@see previewUriFor()) + * Returns a human-readable host relative uri for nodes in the live workspace. + * + * As the human-readable uris are only routed for nodes of the live workspace {@see EventSourcedFrontendNodeRoutePartHandler} + * Absolute preview uris are build for other workspaces {@see previewUriFor} + * + * Cross-linking nodes + * ------------------- * - * Note: Shortcut nodes will be resolved in the RoutePartHandler thus the resulting URI will point - * to the shortcut target (node, asset or external URI) + * Cross linking to a node happens when the site determined based on the current + * route parameters (through the host and sites domain) does not belong to the linked node. + * In this case the domain from the node's site might be used to build a host absolute uri {@see CrossSiteLinkerInterface}. * - * @param NodeAddress $nodeAddress - * @return UriInterface - * @throws NoMatchingRouteException if the node address does not exist + * Host relative urls are build by default for non cross-linked nodes. + * + * Shortcut nodes + * -------------- + * + * Resolving a uri for a shortcut node will result in the url pointing to the shortcut target (node, asset or external URI). + * + * Supported options + * ----------------- + * + * These options will not be considered when building a preview uri {@see previewUriFor} + * + * - forceAbsolute: + * Absolute urls for non cross-linked nodes can be enforced via {@see Options::withForceAbsolute()}. + * In which case the base uri determined by the request is used as host + * instead of a possibly configured site domain's host. + * + * - format: + * A custom format can be specified via {@see Options::withCustomFormat()} + * + * - routingArguments: + * Custom routing arguments can be specified via {@see Options::withCustomRoutingArguments()} + * + * Note that appending additional query parameters can be done + * via {@see UriHelper::uriWithAdditionalQueryParameters()}: + * + * UriHelper::withAdditionalQueryParameters( + * $this->nodeUriBuilder->uriFor(...), + * ['q' => 'search term'] + * ); + * + * @api + * @throws NoMatchingRouteException + * The exception is thrown for various unlike cases in which uri building fails: + * - the default route definitions are misconfigured + * - the custom uri building options don't macht a route + * - the shortcut points to an invalid target + * - the live node address cannot be found in the projection + * Please consult the logs for further information. */ - public function uriFor(NodeAddress $nodeAddress): UriInterface + public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { + $options ??= Options::createEmpty(); + if (!$nodeAddress->workspaceName->isLive()) { // we cannot build a human-readable uri using the showAction as // the DocumentUriPathProjection only handles the live workspace + // now we fall back to building an absolute preview uri ignoring all possible options, because they are not applicable. + // (e.g. otherwise one would need to define a custom json route also for the previewAction which is unlikely considered and untested) return $this->previewUriFor($nodeAddress); } - return new Uri($this->uriBuilder->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos')); + + $routeValues = $options->routingArguments; + $routeValues['node'] = $nodeAddress; + $routeValues['@action'] = strtolower('show'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + if ($options->format !== '') { + $routeValues['@format'] = $options->format; + } + + return $this->router->resolve( + new ResolveContext( + $this->baseUri, + $routeValues, + $options->forceAbsolute, + $this->uriPathPrefix, + $this->routeParameters + ) + ); } /** - * Renders a stable "preview" URI for the given $nodeAddress - * A preview URI is used to display a node that is not public yet (i.e. not in a live workspace). + * Returns a host absolute uri with json encoded node address as query parameter. * - * @param NodeAddress $nodeAddress - * @return UriInterface - * @throws NoMatchingRouteException if the node address does not exist + * Any node address regarding of content repository, or workspace can be linked to. + * Live node address will still be encoded as query parameter and not resolved + * as human friendly url, for that {@see uriFor} must be used. + * + * @api + * @throws NoMatchingRouteException in the unlike case the preview route definition is misconfigured */ public function previewUriFor(NodeAddress $nodeAddress): UriInterface { - return new Uri($this->uriBuilder->uriFor( - 'preview', - ['node' => $nodeAddress->serializeForUri()], - 'Frontend\Node', - 'Neos.Neos' - )); + $routeValues = []; + $routeValues['@action'] = strtolower('preview'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + $previewActionUri = $this->router->resolve( + new ResolveContext( + $this->baseUri, + $routeValues, + true, + $this->uriPathPrefix, + $this->routeParameters + ) + ); + return UriHelper::uriWithAdditionalQueryParameters( + $previewActionUri, + ['node' => $nodeAddress->toJson()] + ); } } diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php new file mode 100644 index 00000000000..be50caaa77e --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php @@ -0,0 +1,47 @@ +getHttpRequest(); + + $baseUri = $this->configuredBaseUri !== null + ? new Uri($this->configuredBaseUri) + : RequestInformationHelper::generateBaseUri($request); + + $routeParameters = $request->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) + ?? RouteParameters::createEmpty(); + + $uriPathPrefix = RequestInformationHelper::getScriptRequestPath($request); + $uriPathPrefix = ltrim($uriPathPrefix, '/'); + + return new NodeUriBuilder($this->router, $baseUri, $uriPathPrefix, $routeParameters); + } +} diff --git a/Neos.Neos/Classes/FrontendRouting/Options.php b/Neos.Neos/Classes/FrontendRouting/Options.php new file mode 100644 index 00000000000..2a7d7a2b87b --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/Options.php @@ -0,0 +1,126 @@ +withCustomFormat('json'); + * + * @api for the factory methods; NOT for the inner state. + */ +final readonly class Options +{ + /** + * @internal the properties themselves are readonly; only the write-methods are API. + * @param array $routingArguments + */ + private function __construct( + public bool $forceAbsolute, + public string $format, + public array $routingArguments, + ) { + } + + /** + * Creates empty options. Chain any of the with* methods to create a new option set with different values. + */ + public static function createEmpty(): self + { + return new self(false, '', []); + } + + /** + * Creates options with option to enforced absolute urls for non cross-linked nodes. + * + * Alias for: + * + * Options::createEmpty()->withForceAbsolute(); + * + */ + public static function createForceAbsolute(): self + { + return new self(true, '', []); + } + + /** + * Option to enforce absolute urls for non cross-linked nodes. + * + * Absolute urls are fully qualified with protocol and host. + */ + public function withForceAbsolute(): self + { + return new self( + true, + $this->format, + $this->routingArguments + ); + } + + /** + * Option to set a custom routing format + * + * In order for the routing framework to match and resolve this format, + * your have to define a custom route in Routes.yaml + * + * - + * name: 'Neos :: Frontend :: Document node with json format' + * uriPattern: '{node}.json' + * defaults: + * '@package': Neos.Neos + * '@controller': Frontend\Node + * '@action': show + * '@format': json + * routeParts: + * node: + * handler: Neos\Neos\Routing\FrontendNodeRoutePartHandlerInterface + * + * See also {@link https://docs.neos.io/guide/rendering/rendering-special-formats} + */ + public function withCustomFormat(string $format): self + { + return new self($this->forceAbsolute, $format, $this->routingArguments); + } + + /** + * Option to set custom routing arguments + * + * Please do not use this functionality to append query parameters + * and use {@see \Neos\Flow\Http\UriHelper::withAdditionalQueryParameters} instead: + * + * UriHelper::withAdditionalQueryParameters( + * $this->nodeUriBuilder->uriFor(...), + * ['q' => 'search term'] + * ); + * + * Appending query parameters via the use of exceeding routing arguments relies + * on `appendExceedingArguments` internally which is discouraged to leverage. + * + * But in case you meant to use routing arguments for advanced uri building, + * you can leverage this low level option. + * + * Be aware in order for the routing framework to match and resolve the arguments, + * your have to define a custom route in Routes.yaml + * + * - + * name: 'Neos :: Frontend :: Document node with pagination' + * uriPattern: '{node}/page-{page}' + * defaults: + * '@package': Neos.Neos + * '@controller': Frontend\Node + * '@action': show + * routeParts: + * node: + * handler: Neos\Neos\Routing\FrontendNodeRoutePartHandlerInterface + * + * @param array $routingArguments + */ + public function withCustomRoutingArguments(array $routingArguments): self + { + return new self($this->forceAbsolute, $this->format, $routingArguments); + } +} diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index bf025e356ea..fc27ab0b5c9 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -16,21 +16,21 @@ use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Fusion\FusionObjects\AbstractFusionObject; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Exception as NeosException; use Neos\Neos\Domain\Model\RenderingMode; -use Neos\Neos\FrontendRouting\NodeAddressFactory; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Fusion\Cache\CacheTag; use Psr\Log\LoggerInterface; @@ -94,6 +94,12 @@ class ConvertUrisImplementation extends AbstractFusionObject */ protected $systemLogger; + /** + * @Flow\Inject + * @var NodeUriBuilderFactory + */ + protected $nodeUriBuilderFactory; + /** * Convert URIs matching a supported scheme with generated URIs * @@ -133,43 +139,40 @@ public function evaluate() return $text; } - $contentRepository = $this->contentRepositoryRegistry->get( - $node->contentRepositoryId - ); - - $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromNode($node); + $nodeAddress = NodeAddress::fromNode($node); $unresolvedUris = []; - $absolute = $this->fusionValue('absolute'); + $options = $this->fusionValue('absolute') ? Options::createForceAbsolute() : Options::createEmpty(); + + $possibleRequest = $this->runtime->fusionGlobals->get('request'); + if ($possibleRequest instanceof ActionRequest) { + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($possibleRequest); + $format = $possibleRequest->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } + } else { + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without it + // this will improve with a reformed uri building: + // https://github.com/neos/flow-development-collection/issues/3354 + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest(ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())); + } - $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($contentRepository, $nodeAddress, &$unresolvedUris, $absolute) { + $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($nodeAddress, &$unresolvedUris, $nodeUriBuilder, $options) { $resolvedUri = null; switch ($matches[1]) { case 'node': - $nodeAddress = $nodeAddress->withNodeAggregateId( + $nodeAddress = $nodeAddress->withAggregateId( NodeAggregateId::fromString($matches[2]) ); - $uriBuilder = new UriBuilder(); - $possibleRequest = $this->runtime->fusionGlobals->get('request'); - if ($possibleRequest instanceof ActionRequest) { - $uriBuilder->setRequest($possibleRequest); - } else { - // unfortunately, the uri-builder always needs a request at hand and cannot build uris without - // even, if the default param merging would not be required - // this will improve with a reformed uri building: - // https://github.com/neos/flow-development-collection/pull/2744 - $uriBuilder->setRequest( - ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) - ); - } - $uriBuilder->setCreateAbsoluteUri($absolute); try { - $resolvedUri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); + $resolvedUri = (string)$nodeUriBuilder->uriFor($nodeAddress, $options); } catch (NoMatchingRouteException) { - $this->systemLogger->info(sprintf('Could not resolve "%s" to a live node uri. Arguments: %s', $matches[0], json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); + // todo log also arguments? + $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $matches[0]), LogEnvironment::fromMethodName(__METHOD__)); } $this->runtime->addCacheTag( - CacheTag::forDynamicNodeAggregate($contentRepository->id, $nodeAddress->workspaceName, NodeAggregateId::fromString($matches[2]))->value + CacheTag::forDynamicNodeAggregate($nodeAddress->contentRepositoryId, $nodeAddress->workspaceName, $nodeAddress->aggregateId)->value ); break; case 'asset': diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index 5cdb3e74f98..d6c92994f74 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -16,27 +16,23 @@ use GuzzleHttp\Psr7\Uri; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\ProtectedContextAwareInterface; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Http\Exception as HttpException; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\FrontendRouting\NodeAddressFactory; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Fusion\ConvertUrisImplementation; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; -/** - * Eel helper for the linking service - */ class LinkHelper implements ProtectedContextAwareInterface { /** @@ -63,6 +59,12 @@ class LinkHelper implements ProtectedContextAwareInterface */ protected $contentRepositoryRegistry; + /** + * @Flow\Inject + * @var NodeUriBuilderFactory + */ + protected $nodeUriBuilderFactory; + /** * @param string|Uri $uri * @return boolean @@ -105,18 +107,17 @@ public function resolveNodeUri( ); return null; } - $contentRepository = $this->contentRepositoryRegistry->get( - $targetNode->contentRepositoryId - ); - $targetNodeAddress = NodeAddressFactory::create($contentRepository)->createFromNode($targetNode); + + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()); + + $options = Options::createEmpty(); + $format = $controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } try { - $targetUri = NodeUriBuilder::fromUriBuilder($controllerContext->getUriBuilder()) - ->uriFor($targetNodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode), $options); + } catch (NoMatchingRouteException $e) { $this->systemLogger->info(sprintf( 'Failed to build URI for node "%s": %e', $targetNode->aggregateId->value, diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 5086aaef33b..69ad10218f8 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -16,15 +16,15 @@ use GuzzleHttp\Psr7\ServerRequest; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\Flow\Mvc\ActionRequest; -use Neos\Neos\FrontendRouting\NodeAddressFactory; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; +use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Psr\Log\LoggerInterface; /** @@ -44,6 +44,12 @@ class NodeUriImplementation extends AbstractFusionObject */ protected $systemLogger; + /** + * @Flow\Inject + * @var NodeUriBuilderFactory + */ + protected $nodeUriBuilderFactory; + /** * A node object or a string node path or NULL to resolve the current document node */ @@ -57,9 +63,9 @@ public function getNode(): Node|string|null * * @return string */ - public function getFormat() + public function getFormat(): string { - return $this->fusionValue('format'); + return (string)$this->fusionValue('format'); } /** @@ -119,13 +125,7 @@ public function evaluate() throw new \RuntimeException(sprintf('Could not find a node instance in Fusion context with name "%s" and no node instance was given to the node argument. Set a node instance in the Fusion context or pass a node object to resolve the URI.', $baseNodeName), 1373100400); } $node = $this->getNode(); - if ($node instanceof Node) { - $contentRepository = $this->contentRepositoryRegistry->get( - $node->contentRepositoryId - ); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $nodeAddress = $nodeAddressFactory->createFromNode($node); - } else { + if (!$node instanceof Node) { throw new \RuntimeException(sprintf('Passing node as %s is not supported yet.', get_debug_type($node))); } /* TODO implement us see https://github.com/neos/neos-development-collection/issues/4524 {@see \Neos\Neos\ViewHelpers\Uri\NodeViewHelper::resolveNodeAddressFromString} for an example implementation @@ -140,30 +140,37 @@ public function evaluate() NodeAggregateId::fromString(\mb_substr($node, 7)) );*/ - $uriBuilder = new UriBuilder(); $possibleRequest = $this->runtime->fusionGlobals->get('request'); if ($possibleRequest instanceof ActionRequest) { - $uriBuilder->setRequest($possibleRequest); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($possibleRequest); } else { - // unfortunately, the uri-builder always needs a request at hand and cannot build uris without - // even, if the default param merging would not be required + // unfortunately, the uri-builder always needs a request at hand and cannot build uris without it // this will improve with a reformed uri building: - // https://github.com/neos/flow-development-collection/pull/2744 - $uriBuilder->setRequest( - ActionRequest::fromHttpRequest(ServerRequest::fromGlobals()) - ); + // https://github.com/neos/flow-development-collection/issues/3354 + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest(ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())); + } + + $options = $this->isAbsolute() ? Options::createForceAbsolute() : Options::createEmpty(); + $format = $this->getFormat() ?: $possibleRequest->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->getAdditionalParams()) { + $options = $options->withCustomRoutingArguments($routingArguments); } - $uriBuilder - ->setFormat($this->getFormat()) - ->setCreateAbsoluteUri($this->isAbsolute()) - ->setArguments($this->getAdditionalParams()) - ->setSection($this->getSection()); try { - return (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); + $resolvedUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($node), $options); } catch (NoMatchingRouteException) { - $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri. Arguments: %s', $node->aggregateId->value, json_encode($uriBuilder->getLastArguments())), LogEnvironment::fromMethodName(__METHOD__)); + // todo log arguments? + $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $node->aggregateId->value), LogEnvironment::fromMethodName(__METHOD__)); + return ''; } - return ''; + + if ($this->getSection() !== '') { + $resolvedUri = $resolvedUri->withFragment($this->getSection()); + } + + return (string)$resolvedUri; } } diff --git a/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php index 599242562e7..9a4250915ec 100644 --- a/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php +++ b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php @@ -15,11 +15,10 @@ namespace Neos\Neos\Routing; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\Neos\FrontendRouting\NodeAddress as LegacyNodeAddress; use Neos\Flow\Annotations as Flow; use Neos\Flow\Aop\JoinPointInterface; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\Neos\FrontendRouting\NodeAddressFactory; /** * Aspect to convert a node object to its context node path. This is used in URI @@ -28,14 +27,12 @@ * On the long term, type converters should be able to convert the reverse direction * as well, and then this aspect could be removed. * + * @deprecated todo remove / repair me for Neos 9: https://github.com/neos/neos-development-collection/issues/5069 * @Flow\Scope("singleton") * @Flow\Aspect */ class NodeIdentityConverterAspect { - #[Flow\Inject] - protected ContentRepositoryRegistry $contentRepositoryRegistry; - /** * Convert the object to its context path, if we deal with ContentRepository nodes. * @@ -47,13 +44,10 @@ public function convertNodeToContextPathForRouting(JoinPointInterface $joinPoint { $objectArgument = $joinPoint->getMethodArgument('object'); if ($objectArgument instanceof Node) { - $contentRepository = $this->contentRepositoryRegistry->get( - $objectArgument->contentRepositoryId - ); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $nodeAddress = $nodeAddressFactory->createFromNode($objectArgument); - return ['__contextNodePath' => $nodeAddress->serializeForUri()]; + return ['__contextNodePath' => NodeAddress::fromNode($objectArgument)->toJson()]; } elseif ($objectArgument instanceof NodeAddress) { + return ['__contextNodePath' => $objectArgument->toJson()]; + } elseif ($objectArgument instanceof LegacyNodeAddress) { return ['__contextNodePath' => $objectArgument->serializeForUri()]; } diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index cd9348a5365..fb520d62994 100644 --- a/Neos.Neos/Classes/Service/LinkingService.php +++ b/Neos.Neos/Classes/Service/LinkingService.php @@ -14,15 +14,18 @@ namespace Neos\Neos\Service; +use GuzzleHttp\Psr7\Uri; use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\BaseUriProvider; use Neos\Flow\Http\Exception as HttpException; +use Neos\Flow\Http\Helper\UriHelper; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\Mvc\Controller\ControllerContext; use Neos\Flow\Property\PropertyMapper; @@ -352,7 +355,7 @@ public function createNodeUri( $mainRequest = $controllerContext->getRequest()->getMainRequest(); $uriBuilder = clone $controllerContext->getUriBuilder(); $uriBuilder->setRequest($mainRequest); - $action = $workspace && $workspace->isPublicWorkspace() && $node->tags->contain(SubtreeTag::disabled()) ? 'show' : 'preview'; + $createLiveUri = $workspace && $workspace->isPublicWorkspace() && $node->tags->contain(SubtreeTag::disabled()); if ($addQueryString === true) { // legacy feature see https://github.com/neos/neos-development-collection/issues/5076 @@ -365,13 +368,27 @@ public function createNodeUri( } } + if (!$createLiveUri) { + $previewActionUri = $uriBuilder + ->reset() + ->setSection($section) + ->setArguments($arguments) + ->setFormat($format ?: $mainRequest->getFormat()) + ->setCreateAbsoluteUri($absolute) + ->uriFor('preview', [], 'Frontend\Node', 'Neos.Neos'); + return (string)UriHelper::uriWithAdditionalQueryParameters( + new Uri($previewActionUri), + ['node' => NodeAddress::fromNode($node)->toJson()] + ); + } + return $uriBuilder ->reset() ->setSection($section) ->setArguments($arguments) ->setFormat($format ?: $mainRequest->getFormat()) ->setCreateAbsoluteUri($absolute) - ->uriFor($action, ['node' => $node], 'Frontend\Node', 'Neos.Neos'); + ->uriFor('show', ['node' => NodeAddress::fromNode($node)], 'Frontend\Node', 'Neos.Neos'); } /** diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 341a2566628..d7990646735 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -14,30 +14,26 @@ namespace Neos\Neos\ViewHelpers\Link; -use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; -use Neos\Neos\Domain\Service\NodeTypeNameFactory; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Http\Exception as HttpException; use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\FluidAdaptor\Core\ViewHelper\AbstractTagBasedViewHelper; use Neos\FluidAdaptor\Core\ViewHelper\Exception as ViewHelperException; use Neos\Fusion\ViewHelpers\FusionContextTrait; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** @@ -150,6 +146,12 @@ class NodeViewHelper extends AbstractTagBasedViewHelper */ protected $throwableStorage; + /** + * @Flow\Inject + * @var NodeUriBuilderFactory + */ + protected $nodeUriBuilderFactory; + /** * @Flow\Inject * @var NodeLabelGeneratorInterface @@ -248,18 +250,11 @@ public function render(): string } if ($node instanceof Node) { - $contentRepository = $this->contentRepositoryRegistry->get( - $node->contentRepositoryId - ); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $nodeAddress = $nodeAddressFactory->createFromNode($node); + $nodeAddress = NodeAddress::fromNode($node); } elseif (is_string($node)) { $documentNode = $this->getContextVariable('documentNode'); assert($documentNode instanceof Node); - $contentRepository = $this->contentRepositoryRegistry->get( - $documentNode->contentRepositoryId - ); - $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode, $contentRepository); + $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode); $node = $documentNode; } else { throw new ViewHelperException(sprintf( @@ -269,18 +264,18 @@ public function render(): string ), 1601372376); } - + $contentRepository = $this->contentRepositoryRegistry->get($nodeAddress->contentRepositoryId); $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName) ->getSubgraph( $nodeAddress->dimensionSpacePoint, $node->visibilityConstraints ); - $resolvedNode = $subgraph->findNodeById($nodeAddress->nodeAggregateId); + $resolvedNode = $subgraph->findNodeById($nodeAddress->aggregateId); if ($resolvedNode === null) { $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( 'Failed to resolve node "%s" in workspace "%s" and dimension %s', - $nodeAddress->nodeAggregateId->value, + $nodeAddress->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1601372444)); @@ -288,12 +283,11 @@ public function render(): string if ($resolvedNode && $this->getNodeType($resolvedNode)->isOfType(NodeTypeNameFactory::NAME_SHORTCUT)) { try { $shortcutNodeAddress = $this->nodeShortcutResolver->resolveShortcutTarget( - $nodeAddress, - $contentRepository + $nodeAddress ); if ($shortcutNodeAddress instanceof NodeAddress) { $resolvedNode = $subgraph - ->findNodeById($shortcutNodeAddress->nodeAggregateId); + ->findNodeById($shortcutNodeAddress->aggregateId); } } catch (NodeNotFoundException | InvalidShortcutException $e) { $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( @@ -305,28 +299,32 @@ public function render(): string } } - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->controllerContext->getRequest()->getMainRequest()); - $uriBuilder->setFormat($this->arguments['format']) - ->setCreateAbsoluteUri($this->arguments['absolute']) - ->setArguments($this->arguments['arguments']) - ->setSection($this->arguments['section']); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); + + $options = $this->arguments['absolute'] ? Options::createForceAbsolute() : Options::createEmpty(); + $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->arguments['arguments']) { + $options = $options->withCustomRoutingArguments($routingArguments); + } $uri = ''; try { - $uri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $uri = $nodeUriBuilder->uriFor($nodeAddress, $options); + + if ($this->arguments['section'] !== '') { + $uri = $uri->withFragment($this->arguments['section']); + } + } catch (NoMatchingRouteException $e) { $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( 'Failed to build URI for node: %s: %s', - $nodeAddress, + $nodeAddress->toJson(), $e->getMessage() ), 1601372594, $e)); } - $this->tag->addAttribute('href', $uri); + $this->tag->addAttribute('href', (string)$uri); $this->templateVariableContainer->add($this->arguments['nodeVariableName'], $resolvedNode); $content = $this->renderChildren(); @@ -346,19 +344,16 @@ public function render(): string * and "~" to the corresponding NodeAddress * * @param string $path - * @return NodeAddress * @throws ViewHelperException */ - private function resolveNodeAddressFromString( - string $path, - Node $documentNode, - ContentRepository $contentRepository - ): NodeAddress { - /* @var Node $documentNode */ - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $documentNodeAddress = $nodeAddressFactory->createFromNode($documentNode); + private function resolveNodeAddressFromString(string $path, Node $documentNode): NodeAddress + { + $contentRepository = $this->contentRepositoryRegistry->get( + $documentNode->contentRepositoryId + ); + $documentNodeAddress = NodeAddress::fromNode($documentNode); if (strncmp($path, 'node://', 7) === 0) { - return $documentNodeAddress->withNodeAggregateId( + return $documentNodeAddress->withAggregateId( NodeAggregateId::fromString(\mb_substr($path, 7)) ); } @@ -367,11 +362,11 @@ private function resolveNodeAddressFromString( VisibilityConstraints::withoutRestrictions() ); if (strncmp($path, '~', 1) === 0) { - $siteNode = $subgraph->findClosestNode($documentNodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + $siteNode = $subgraph->findClosestNode($documentNodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); if ($siteNode === null) { throw new ViewHelperException(sprintf( 'Failed to determine site node for aggregate node "%s" in workspace "%s" and dimension %s', - $documentNodeAddress->nodeAggregateId->value, + $documentNodeAddress->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1601366598); @@ -394,11 +389,11 @@ private function resolveNodeAddressFromString( throw new ViewHelperException(sprintf( 'Node on path "%s" could not be found for aggregate node "%s" in workspace "%s" and dimension %s', $path, - $documentNodeAddress->nodeAggregateId->value, + $documentNodeAddress->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1601311789); } - return $documentNodeAddress->withNodeAggregateId($targetNode->aggregateId); + return $documentNodeAddress->withAggregateId($targetNode->aggregateId); } } diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 15739d29ef7..79e7e997a26 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -16,23 +16,20 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindClosestNodeFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\Projection\ContentGraph\NodePath; -use Neos\Neos\Domain\Service\NodeTypeNameFactory; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Http\Exception as HttpException; use Neos\Flow\Log\ThrowableStorageInterface; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\Exception\MissingActionNameException; -use Neos\Flow\Mvc\Routing\UriBuilder; use Neos\FluidAdaptor\Core\ViewHelper\AbstractViewHelper; use Neos\FluidAdaptor\Core\ViewHelper\Exception as ViewHelperException; use Neos\Fusion\ViewHelpers\FusionContextTrait; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; /** * A view helper for creating URIs pointing to nodes. @@ -118,6 +115,13 @@ class NodeViewHelper extends AbstractViewHelper */ protected $throwableStorage; + /** + * @Flow\Inject + * @var NodeUriBuilderFactory + */ + protected $nodeUriBuilderFactory; + + /** * Initialize arguments * @@ -192,14 +196,13 @@ public function render(): string $node = $this->getContextVariable($this->arguments['baseNodeName']); } + /* @var Node $documentNode */ + $documentNode = $this->getContextVariable('documentNode'); + if ($node instanceof Node) { - $contentRepository = $this->contentRepositoryRegistry->get( - $node->contentRepositoryId - ); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $nodeAddress = $nodeAddressFactory->createFromNode($node); + $nodeAddress = NodeAddress::fromNode($node); } elseif (is_string($node)) { - $nodeAddress = $this->resolveNodeAddressFromString($node); + $nodeAddress = $this->resolveNodeAddressFromString($node, $documentNode); } else { throw new ViewHelperException(sprintf( 'The "node" argument can only be a string or an instance of %s. Given: %s', @@ -208,31 +211,32 @@ public function render(): string ), 1601372376); } - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($this->controllerContext->getRequest()); - $uriBuilder->setFormat($this->arguments['format']) - ->setCreateAbsoluteUri($this->arguments['absolute']) - ->setArguments($this->arguments['arguments']) - ->setSection($this->arguments['section']); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); - $uri = ''; - if (!$nodeAddress) { - return ''; + $options = $this->arguments['absolute'] ? Options::createForceAbsolute() : Options::createEmpty(); + $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->arguments['arguments']) { + $options = $options->withCustomRoutingArguments($routingArguments); } + + $uri = ''; try { - $uri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $uri = $nodeUriBuilder->uriFor($nodeAddress, $options); + + if ($this->arguments['section'] !== '') { + $uri = $uri->withFragment($this->arguments['section']); + } + } catch (NoMatchingRouteException $e) { $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( 'Failed to build URI for node: %s: %s', - $nodeAddress, + $nodeAddress->toJson(), $e->getMessage() ), 1601372594, $e)); } - return $uri; + return (string)$uri; } /** @@ -240,20 +244,16 @@ public function render(): string * to the corresponding NodeAddress * * @param string $path - * @return \Neos\Neos\FrontendRouting\NodeAddress * @throws ViewHelperException */ - private function resolveNodeAddressFromString(string $path): ?NodeAddress + private function resolveNodeAddressFromString(string $path, Node $documentNode): NodeAddress { - /* @var Node $documentNode */ - $documentNode = $this->getContextVariable('documentNode'); $contentRepository = $this->contentRepositoryRegistry->get( $documentNode->contentRepositoryId ); - $nodeAddressFactory = NodeAddressFactory::create($contentRepository); - $documentNodeAddress = $nodeAddressFactory->createFromNode($documentNode); + $documentNodeAddress = NodeAddress::fromNode($documentNode); if (strncmp($path, 'node://', 7) === 0) { - return $documentNodeAddress->withNodeAggregateId( + return $documentNodeAddress->withAggregateId( NodeAggregateId::fromString(\mb_substr($path, 7)) ); } @@ -262,11 +262,11 @@ private function resolveNodeAddressFromString(string $path): ?NodeAddress VisibilityConstraints::withoutRestrictions() ); if (strncmp($path, '~', 1) === 0) { - $siteNode = $subgraph->findClosestNode($documentNodeAddress->nodeAggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); + $siteNode = $subgraph->findClosestNode($documentNodeAddress->aggregateId, FindClosestNodeFilter::create(nodeTypes: NodeTypeNameFactory::NAME_SITE)); if ($siteNode === null) { throw new ViewHelperException(sprintf( 'Failed to determine site node for aggregate node "%s" in workspace "%s" and dimension %s', - $documentNodeAddress->nodeAggregateId->value, + $documentNodeAddress->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() ), 1601366598); @@ -286,15 +286,14 @@ private function resolveNodeAddressFromString(string $path): ?NodeAddress ); } if ($targetNode === null) { - $this->throwableStorage->logThrowable(new ViewHelperException(sprintf( + throw new ViewHelperException(sprintf( 'Node on path "%s" could not be found for aggregate node "%s" in workspace "%s" and dimension %s', $path, - $documentNodeAddress->nodeAggregateId->value, + $documentNodeAddress->aggregateId->value, $subgraph->getWorkspaceName()->value, $subgraph->getDimensionSpacePoint()->toJson() - ), 1601311789)); - return null; + ), 1601311789); } - return $documentNodeAddress->withNodeAggregateId($targetNode->aggregateId); + return $documentNodeAddress->withAggregateId($targetNode->aggregateId); } } diff --git a/Neos.Neos/Configuration/Routes.Frontend.yaml b/Neos.Neos/Configuration/Routes.Frontend.yaml index d67077c2e6b..164552278ca 100644 --- a/Neos.Neos/Configuration/Routes.Frontend.yaml +++ b/Neos.Neos/Configuration/Routes.Frontend.yaml @@ -7,7 +7,6 @@ uriPattern: 'neos/preview' defaults: '@action': 'preview' - appendExceedingArguments: true - name: 'Default Frontend' diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 07957742e4e..a8c15990295 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -19,16 +19,15 @@ use GuzzleHttp\Psr7\Uri; use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; -use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables; +use Neos\Flow\Configuration\ConfigurationManager; use Neos\Flow\Http\ServerRequestAttributes; -use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\Dto\RouteContext; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\RouterInterface; -use Neos\Flow\ObjectManagement\ObjectManagerInterface; use Neos\Flow\Persistence\PersistenceManagerInterface; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\Tests\FunctionalTestRequestHandler; @@ -43,9 +42,7 @@ use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\FrontendRouting\DimensionResolution\DimensionResolverFactoryInterface; use Neos\Neos\FrontendRouting\DimensionResolution\RequestToDimensionSpacePointContext; -use Neos\Neos\FrontendRouting\NodeAddress; -use Neos\Neos\FrontendRouting\NodeAddressFactory; -use Neos\Neos\FrontendRouting\NodeUriBuilder; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjectionFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionMiddleware; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; @@ -183,17 +180,19 @@ public function iAmOnUrl(string $url): void */ public function theMatchedNodeShouldBeInContentStreamAndOriginDimension(string $nodeAggregateId, string $contentStreamId, string $dimensionSpacePoint): void { - $nodeAddress = $this->match($this->requestUrl); - Assert::assertNotNull($nodeAddress, 'Routing result does not have "node" key - this probably means that the FrontendNodeRoutePartHandler did not properly resolve the result.'); - Assert::assertTrue($nodeAddress->isInLiveWorkspace()); - Assert::assertSame($nodeAggregateId, $nodeAddress->nodeAggregateId->value); - Assert::assertSame($contentStreamId, $nodeAddress->contentStreamId->value); + $matchedNodeAddress = $this->match($this->requestUrl); + Assert::assertNotNull($matchedNodeAddress, 'Routing result does not have "node" key - this probably means that the FrontendNodeRoutePartHandler did not properly resolve the result.'); + Assert::assertTrue($matchedNodeAddress->workspaceName->isLive()); + Assert::assertSame($nodeAggregateId, $matchedNodeAddress->aggregateId->value); + // todo useless check? + $workspace = $this->currentContentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId(ContentStreamId::fromString($contentStreamId)); + Assert::assertSame($contentStreamId, $workspace?->currentContentStreamId->value); Assert::assertSame( DimensionSpacePoint::fromJsonString($dimensionSpacePoint), - $nodeAddress->dimensionSpacePoint, + $matchedNodeAddress->dimensionSpacePoint, sprintf( 'Dimension space point "%s" did not match the expected "%s"', - $nodeAddress->dimensionSpacePoint->toJson(), + $matchedNodeAddress->dimensionSpacePoint->toJson(), $dimensionSpacePoint ) ); @@ -205,7 +204,7 @@ public function theMatchedNodeShouldBeInContentStreamAndOriginDimension(string $ public function noNodeShouldMatchUrl(string $url): void { $matchedNodeAddress = $this->match(new Uri($url)); - Assert::assertNull($matchedNodeAddress, 'Expected no node to be found, but instead the following node address was matched: ' . $matchedNodeAddress ?? '- none -'); + Assert::assertNull($matchedNodeAddress, 'Expected no node to be found, but instead the following node address was matched: ' . $matchedNodeAddress?->toJson() ?? '- none -'); } /** @@ -216,8 +215,11 @@ public function theUrlShouldMatchTheNodeInContentStreamAndDimension(string $url, $matchedNodeAddress = $this->match(new Uri($url)); Assert::assertNotNull($matchedNodeAddress, 'Expected node to be found, but instead nothing was found.'); - Assert::assertEquals(NodeAggregateId::fromString($nodeAggregateId), $matchedNodeAddress->nodeAggregateId, 'Expected nodeAggregateId doesn\'t match.'); - Assert::assertEquals(ContentStreamId::fromString($contentStreamId), $matchedNodeAddress->contentStreamId, 'Expected contentStreamId doesn\'t match.'); + Assert::assertEquals(NodeAggregateId::fromString($nodeAggregateId), $matchedNodeAddress->aggregateId, 'Expected nodeAggregateId doesn\'t match.'); + + // todo use workspace name instead here: + $workspace = $this->currentContentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId(ContentStreamId::fromString($contentStreamId)); + Assert::assertEquals($workspace->workspaceName, $matchedNodeAddress->workspaceName, 'Expected workspace doesn\'t match.'); Assert::assertTrue($matchedNodeAddress->dimensionSpacePoint->equals(DimensionSpacePoint::fromJsonString($dimensionSpacePoint)), 'Expected dimensionSpacePoint doesn\'t match.'); } @@ -241,8 +243,7 @@ private function match(UriInterface $uri): ?NodeAddress return null; } - $nodeAddressFactory = NodeAddressFactory::create($this->currentContentRepository); - return $nodeAddressFactory->createFromUriString($routeValues['node']); + return NodeAddress::fromJsonString($routeValues['node']); } @@ -261,6 +262,13 @@ public function theNodeShouldResolveToUrl(string $nodeAggregateId, string $conte */ public function theNodeShouldNotResolve(string $nodeAggregateId, string $contentStreamId, string $dimensionSpacePoint): void { + if ( + ($this->getObject(ConfigurationManager::class) + ->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Neos.Flow.mvc.routes')['Neos.Flow'] ?? false) !== false + ) { + Assert::fail('In this distribution the Flow routes are included into the global configuration and thus any route arguments will always resolve. Please set in Neos.Flow.mvc.routes "Neos.Flow": false.'); + } + $resolvedUrl = null; $exception = false; try { @@ -296,18 +304,22 @@ private function resolveUrl(string $nodeAggregateId, string $contentStreamId, st if ($this->requestUrl === null) { $this->iAmOnUrl('/'); } - $nodeAddress = new NodeAddress( - ContentStreamId::fromString($contentStreamId), + $workspace = $this->currentContentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId(ContentStreamId::fromString($contentStreamId)); + + $nodeAddress = NodeAddress::create( + $this->currentContentRepository->id, + $workspace->workspaceName, // todo always live? DimensionSpacePoint::fromJsonString($dimensionSpacePoint), \str_starts_with($nodeAggregateId, '$') ? $this->rememberedNodeAggregateIds[\mb_substr($nodeAggregateId, 1)] - : NodeAggregateId::fromString($nodeAggregateId), - WorkspaceName::forLive() + : NodeAggregateId::fromString($nodeAggregateId) ); $httpRequest = $this->getObject(ServerRequestFactoryInterface::class)->createServerRequest('GET', $this->requestUrl); $httpRequest = $this->addRoutingParameters($httpRequest); - $actionRequest = ActionRequest::fromHttpRequest($httpRequest); - return NodeUriBuilder::fromRequest($actionRequest)->uriFor($nodeAddress); + + return $this->getObject(NodeUriBuilderFactory::class) + ->forActionRequest(\Neos\Flow\Mvc\ActionRequest::fromHttpRequest($httpRequest)) + ->uriFor($nodeAddress); } private function addRoutingParameters(ServerRequestInterface $httpRequest): ServerRequestInterface diff --git a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php index 515fc39f2b7..a76443dd849 100644 --- a/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php +++ b/Neos.Workspace.Ui/Classes/Controller/WorkspaceController.php @@ -30,6 +30,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; use Neos\ContentRepository\Core\SharedModel\User\UserId; use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId; @@ -62,8 +63,9 @@ use Neos\Neos\Domain\Workspace\DiscardAllChanges; use Neos\Neos\Domain\Workspace\PublishAllChanges; use Neos\Neos\Domain\Workspace\WorkspaceProvider; -use Neos\Neos\FrontendRouting\NodeAddress; +use Neos\Neos\FrontendRouting\NodeAddress as LegacyNodeAddress; use Neos\Neos\FrontendRouting\NodeAddressFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\PendingChangesProjection\ChangeFinder; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; @@ -80,6 +82,9 @@ class WorkspaceController extends AbstractModuleController #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; + #[Flow\Inject] protected SiteRepository $siteRepository; @@ -476,31 +481,36 @@ public function rebaseAndRedirectAction(Node $targetNode, Workspace $targetWorks * } */ - $targetNodeAddressInPersonalWorkspace = new NodeAddress( - $personalWorkspace->currentContentStreamId, + $targetNodeAddressInPersonalWorkspace = NodeAddress::create( + $targetNode->contentRepositoryId, + $personalWorkspace->workspaceName, $targetNode->dimensionSpacePoint, - $targetNode->aggregateId, - $personalWorkspace->workspaceName + $targetNode->aggregateId ); - $mainRequest = $this->controllerContext->getRequest()->getMainRequest(); - /** @var ActionRequest $mainRequest */ - $this->uriBuilder->setRequest($mainRequest); - if ($this->packageManager->isPackageAvailable('Neos.Neos.Ui')) { + // todo remove me legacy + $legacyTargetNodeAddressInPersonalWorkspace = new LegacyNodeAddress( + $personalWorkspace->currentContentStreamId, + $targetNodeAddressInPersonalWorkspace->dimensionSpacePoint, + $targetNodeAddressInPersonalWorkspace->aggregateId, + $targetNodeAddressInPersonalWorkspace->workspaceName + ); + $mainRequest = $this->controllerContext->getRequest()->getMainRequest(); + /** @var ActionRequest $mainRequest */ + $this->uriBuilder->setRequest($mainRequest); + $this->redirect( 'index', 'Backend', 'Neos.Neos.Ui', - ['node' => $targetNodeAddressInPersonalWorkspace] + ['node' => $legacyTargetNodeAddressInPersonalWorkspace] ); } - $this->redirect( - 'show', - 'Frontend\\Node', - 'Neos.Workspace.Ui', - ['node' => $targetNodeAddressInPersonalWorkspace] + $this->redirectToUri( + $this->nodeUriBuilderFactory->forActionRequest($this->request) + ->uriFor($targetNodeAddressInPersonalWorkspace) ); } @@ -823,7 +833,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos // As for changes of type `delete` we are using nodes from the live content stream // we can't create `serializedNodeAddress` from the node. // Instead, we use the original stored values. - $nodeAddress = new NodeAddress( + $nodeAddress = new LegacyNodeAddress( $change->contentStreamId, $change->originDimensionSpacePoint->toDimensionSpacePoint(), $change->nodeAggregateId,