From ba13da30b331cd08f59f03a6f83cd11cb34d9db4 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:03:54 +0100 Subject: [PATCH 01/19] WIP: New `NodeUriBuilder` and `NodeUriSpecification` --- .../FrontendRouting/NodeUriBuilder.php | 152 ++++++++++++------ .../FrontendRouting/NodeUriBuilderFactory.php | 38 +++++ .../FrontendRouting/NodeUriSpecification.php | 45 ++++++ 3 files changed, 186 insertions(+), 49 deletions(-) create mode 100644 Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php create mode 100644 Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index 0e7a23493e..ff72c7a260 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -14,80 +14,134 @@ namespace Neos\Neos\FrontendRouting; -use GuzzleHttp\Psr7\Uri; -use Neos\Flow\Annotations as Flow; -use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; -use Neos\Flow\Mvc\Routing\UriBuilder; +use Neos\Flow\Mvc\Routing\Dto\ResolveContext; +use Neos\Flow\Mvc\Routing\Dto\RouteParameters; +use Neos\Flow\Mvc\Routing\RouterInterface; use Psr\Http\Message\UriInterface; -/** - * 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) - */ final class NodeUriBuilder { - private UriBuilder $uriBuilder; + /** + * @internal + * + * This context ($baseUri and $routeParameters) can be inferred from the current request. + * + * For generating node uris in cli context, you can leverage `fromBaseUri` and pass in the desired base uri, + * Wich will be used for when generating host absolute uris. + * If the base uri does not contain a host, absolute uris which would contain the host of the current request + * like from `absoluteUriFor`, will be generated without host. + * + * TODO Flows base uri configuration is ignored if not specifically added via `mergeBaseUri` + */ + public function __construct( + private readonly RouterInterface $router, + private readonly UriInterface $baseUri, + private readonly RouteParameters $routeParameters + ) { + } /** - * @Flow\Autowiring(false) + * Return human readable host relative uris if the cr of the current request matches the one of the specified node. + * For cross-links to another cr the resulting uri be absolute and contain the host of the other site's domain. + * + * As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection) + * This method requires the node to be passed to be in the live workspace and will throw otherwise. + * + * @throws NoMatchingRouteException */ - private function __construct(UriBuilder $uriBuilder) + public function uriFor(NodeUriSpecification $specification): UriInterface { - $this->uriBuilder = $uriBuilder; + return $this->router->resolve( + new ResolveContext( + $this->baseUri, + $this->toShowActionRouteValues($specification), + false, + ltrim($this->baseUri->getPath(), '\/'), + $this->routeParameters + ) + ); } - public static function fromRequest(ActionRequest $request): self + /** + * Return human readable absolute uris with host, independent if the node is cross linked or of the current request. + * For nodes of the current cr the passed base uri will be used as host. For cross-linked nodes the host will be derived by the site's domain. + * + * As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection) + * This method requires the node to be passed to be in the live workspace and will throw otherwise. + * + * @throws NoMatchingRouteException + */ + public function absoluteUriFor(NodeUriSpecification $specification): UriInterface { - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - - return new self($uriBuilder); + return $this->router->resolve( + new ResolveContext( + $this->baseUri, + $this->toShowActionRouteValues($specification), + true, + ltrim($this->baseUri->getPath(), '\/'), + $this->routeParameters + ) + ); } - public static function fromUriBuilder(UriBuilder $uriBuilder): self + /** + * Returns a host relative uri with fully qualified node as query parameter encoded. + */ + public function previewUriFor(NodeUriSpecification $specification): UriInterface { - return new self($uriBuilder); + return $this->router->resolve( + new ResolveContext( + $this->baseUri, + $this->toPreviewActionRouteValues($specification), + false, + ltrim($this->baseUri->getPath(), '\/'), + $this->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()) - * - * Note: Shortcut nodes will be resolved in the RoutePartHandler thus the resulting URI will point - * to the shortcut target (node, asset or external URI) - * - * @param NodeAddress $nodeAddress - * @return UriInterface - * @throws NoMatchingRouteException if the node address does not exist + * @return array */ - public function uriFor(NodeAddress $nodeAddress): UriInterface + private function toShowActionRouteValues(NodeUriSpecification $specification): array { - if (!$nodeAddress->workspaceName->isLive()) { - // we cannot build a human-readable uri using the showAction as - // the DocumentUriPathProjection only handles the live workspace - return $this->previewUriFor($nodeAddress); + $routeValues = $specification->routingArguments; + $routeValues['node'] = $specification->node; + $routeValues['@action'] = strtolower('show'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + if ($specification->format !== '') { + $routeValues['@format'] = $specification->format; } - return new Uri($this->uriBuilder->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos')); + return $routeValues; } /** - * 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). - * - * @param NodeAddress $nodeAddress - * @return UriInterface - * @throws NoMatchingRouteException if the node address does not exist + * @return array */ - public function previewUriFor(NodeAddress $nodeAddress): UriInterface + private function toPreviewActionRouteValues(NodeUriSpecification $specification): array { - return new Uri($this->uriBuilder->uriFor( - 'preview', - ['node' => $nodeAddress->serializeForUri()], - 'Frontend\Node', - 'Neos.Neos' - )); + // todo fully migrate me to NodeUriSpecification + $reg = \Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\ContentRepositoryRegistry\ContentRepositoryRegistry::class); + $ws = $reg->get($specification->node->contentRepositoryId)->getWorkspaceFinder()->findOneByName($specification->node->workspaceName); + $nodeAddress = new NodeAddress( + $ws->currentContentStreamId ?? throw new \Exception(), + $specification->node->dimensionSpacePoint, + $specification->node->aggregateId, + $specification->node->workspaceName, + ); + + $routeValues = $specification->routingArguments; + $routeValues['node'] = $nodeAddress->serializeForUri(); + $routeValues['@action'] = strtolower('preview'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + if ($specification->format !== '') { + $routeValues['@format'] = $specification->format; + } + return $routeValues; } } diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php new file mode 100644 index 0000000000..797f55f1f8 --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php @@ -0,0 +1,38 @@ +getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) + ?? RouteParameters::createEmpty(); + return new NodeUriBuilder($this->router, $baseUri, $routeParameters); + } + + public function forBaseUri(UriInterface $baseUri): NodeUriBuilder + { + // todo??? + // $siteDetectionResult = SiteDetectionResult::fromRequest(new ServerRequest(method: 'GET', uri: $baseUri)); + // $routeParameters = $siteDetectionResult->storeInRouteParameters(RouteParameters::createEmpty()); + return new NodeUriBuilder($this->router, $baseUri, RouteParameters::createEmpty()); + } +} diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php b/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php new file mode 100644 index 0000000000..d586247796 --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php @@ -0,0 +1,45 @@ + $routingArguments + */ + private function __construct( + public NodeAddress $node, + public string $format, + public array $routingArguments, + ) { + } + + public static function create(NodeAddress $node): self + { + return new self($node, '', []); + } + + public function withFormat(string $format): self + { + return new self($this->node, $format, $this->routingArguments); + } + + /** + * @deprecated if you meant to append query parameters, + * please use {@see \Neos\Flow\Http\UriHelper::withAdditionalQueryParameters} instead: + * + * ```php + * UriHelper::withAdditionalQueryParameters($this->nodeUriBuilder->uriFor(...), ['q' => 'search term']); + * ``` + * + * @param array $routingArguments + */ + public function withRoutingArguments(array $routingArguments): self + { + return new self($this->node, $this->format, $routingArguments); + } +} From cf8c35b4f662be93a999862a7deee26ccfccaa89 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 13 May 2024 20:57:00 +0200 Subject: [PATCH 02/19] WIP: Adjust to use new NodeAddress --- .../Controller/Frontend/NodeController.php | 68 ++++++------ ...entSourcedFrontendNodeRoutePartHandler.php | 17 ++- .../FrontendNodeRoutePartHandlerInterface.php | 2 - .../Classes/FrontendRouting/NodeAddress.php | 11 +- .../FrontendRouting/NodeAddressFactory.php | 24 ++++- .../FrontendRouting/NodeShortcutResolver.php | 25 ++--- .../FrontendRouting/NodeUriBuilder.php | 2 - .../FrontendRouting/NodeUriBuilderFactory.php | 10 +- .../Fusion/ConvertUrisImplementation.php | 39 +++---- .../Classes/Fusion/Helper/LinkHelper.php | 32 +++--- .../Classes/Fusion/NodeUriImplementation.php | 56 +++++----- .../Routing/NodeIdentityConverterAspect.php | 18 ++-- .../ViewHelpers/Link/NodeViewHelper.php | 100 ++++++++---------- .../ViewHelpers/Uri/NodeViewHelper.php | 86 +++++++-------- .../Features/Bootstrap/RoutingTrait.php | 61 ++++++----- 15 files changed, 269 insertions(+), 282 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 4fc24f62bf..f86a3c2dae 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,10 @@ 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\NodeUriSpecification; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Neos\View\FusionView; @@ -106,6 +106,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 +133,15 @@ public function previewAction(string $node): void $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); - $nodeAddress = NodeAddressFactory::create($contentRepository)->createFromUriString($node); + // todo temporary legacy + $nodeAddress = NodeAddressFactory::create($contentRepository)->createCoreNodeAddressFromLegacyUriString($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 +150,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 +196,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 +270,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->forRequest($this->request->getHttpRequest()) + ->uriFor(NodeUriSpecification::create($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 a149cdb6f4..4a9ebf3095 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; @@ -240,12 +241,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,7 +263,6 @@ 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; } @@ -284,9 +285,6 @@ 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 */ @@ -294,13 +292,12 @@ private function resolveNodeAddress( NodeAddress $nodeAddress, SiteDetectionResult $currentRequestSiteDetectionResult ): 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 ); diff --git a/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php b/Neos.Neos/Classes/FrontendRouting/FrontendNodeRoutePartHandlerInterface.php index 40766ae965..feb754ba03 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 1126c00bab..0f71cdaa16 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 7d18725167..55aa6654a6 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 @@ -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 94ad0a81ad..2f3d563062 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 ff72c7a260..212df0c5b9 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -31,8 +31,6 @@ final class NodeUriBuilder * Wich will be used for when generating host absolute uris. * If the base uri does not contain a host, absolute uris which would contain the host of the current request * like from `absoluteUriFor`, will be generated without host. - * - * TODO Flows base uri configuration is ignored if not specifically added via `mergeBaseUri` */ public function __construct( private readonly RouterInterface $router, diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php index 797f55f1f8..7438d5c5d2 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php @@ -10,7 +10,6 @@ use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\RouterInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\UriInterface; #[Flow\Scope('singleton')] final class NodeUriBuilderFactory @@ -22,17 +21,10 @@ public function __construct( public function forRequest(ServerRequestInterface $request): NodeUriBuilder { + // TODO Flows base uri configuration is currently ignored $baseUri = RequestInformationHelper::generateBaseUri($request); $routeParameters = $request->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) ?? RouteParameters::createEmpty(); return new NodeUriBuilder($this->router, $baseUri, $routeParameters); } - - public function forBaseUri(UriInterface $baseUri): NodeUriBuilder - { - // todo??? - // $siteDetectionResult = SiteDetectionResult::fromRequest(new ServerRequest(method: 'GET', uri: $baseUri)); - // $routeParameters = $siteDetectionResult->storeInRouteParameters(RouteParameters::createEmpty()); - return new NodeUriBuilder($this->router, $baseUri, RouteParameters::createEmpty()); - } } diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index d32f795f49..23e4aca025 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\NodeUriSpecification; 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,38 @@ 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'); - $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, $absolute) { $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); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($possibleRequest->getHttpRequest()); } 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()) - ); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); } - $uriBuilder->setCreateAbsoluteUri($absolute); try { - $resolvedUri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); + $resolvedUri = $absolute + ? (string)$nodeUriBuilder->absoluteUriFor(NodeUriSpecification::create($nodeAddress)) + : (string)$nodeUriBuilder->uriFor(NodeUriSpecification::create($nodeAddress)); } 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 5cdb3e74f9..a4c246df72 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\NodeUriSpecification; 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,12 @@ public function resolveNodeUri( ); return null; } - $contentRepository = $this->contentRepositoryRegistry->get( - $targetNode->contentRepositoryId - ); - $targetNodeAddress = NodeAddressFactory::create($contentRepository)->createFromNode($targetNode); + + $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($controllerContext->getRequest()->getHttpRequest()); + try { - $targetUri = NodeUriBuilder::fromUriBuilder($controllerContext->getUriBuilder()) - ->uriFor($targetNodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $targetUri = $nodeUriBuilder->uriFor(NodeUriSpecification::create(NodeAddress::fromNode($targetNode))); + } 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 5086aaef33..5a0f08d416 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\NodeUriSpecification; 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,34 @@ 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->forRequest($possibleRequest->getHttpRequest()); } 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()) - ); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); } - $uriBuilder - ->setFormat($this->getFormat()) - ->setCreateAbsoluteUri($this->isAbsolute()) - ->setArguments($this->getAdditionalParams()) - ->setSection($this->getSection()); + $specification = NodeUriSpecification::create(NodeAddress::fromNode($node)) + ->withFormat($this->getFormat() ?: '') + ->withRoutingArguments($this->getAdditionalParams()); try { - return (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); + $resolvedUri = $this->isAbsolute() + ? $nodeUriBuilder->absoluteUriFor($specification) + : $nodeUriBuilder->uriFor($specification); } 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 599242562e..9a4250915e 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/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 341a256662..6aefb4bf9d 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\NodeUriSpecification; 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,29 @@ 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->forRequest($this->controllerContext->getRequest()->getHttpRequest()); $uri = ''; + $specification = NodeUriSpecification::create($nodeAddress) + ->withFormat($this->arguments['format'] ?: '') + ->withRoutingArguments($this->arguments['arguments']); + try { - $uri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $uri = $this->arguments['absolute'] + ? $nodeUriBuilder->absoluteUriFor($specification) + : $nodeUriBuilder->uriFor($specification); + + 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 +341,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 +359,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 +386,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 15739d29ef..2f249e5cf0 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\NodeUriSpecification; /** * 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,29 @@ 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->forRequest($this->controllerContext->getRequest()->getHttpRequest()); $uri = ''; - if (!$nodeAddress) { - return ''; - } + $specification = NodeUriSpecification::create($nodeAddress) + ->withFormat($this->arguments['format'] ?: '') + ->withRoutingArguments($this->arguments['arguments']); + try { - $uri = (string)NodeUriBuilder::fromUriBuilder($uriBuilder)->uriFor($nodeAddress); - } catch ( - HttpException - | NoMatchingRouteException - | MissingActionNameException $e - ) { + $uri = $this->arguments['absolute'] + ? $nodeUriBuilder->absoluteUriFor($specification) + : $nodeUriBuilder->uriFor($specification); + + 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 +241,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 +259,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 +283,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/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 07957742e4..819aabe944 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,8 @@ 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\NodeUriSpecification; use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjectionFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionMiddleware; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; @@ -183,17 +181,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 +205,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 +216,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 +244,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 +263,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 +305,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) + ->forRequest($httpRequest) + ->uriFor(NodeUriSpecification::create($nodeAddress)); } private function addRoutingParameters(ServerRequestInterface $httpRequest): ServerRequestInterface From c8246040d2ca54b1a925a4e7ca80cc3f38e4204b Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 18 May 2024 19:10:54 +0200 Subject: [PATCH 03/19] FEATURE: Allow new nodeAddress to be serialized into uri --- .../DimensionSpace/DimensionSpacePoint.php | 5 ++ .../Classes/SharedModel/Node/NodeAddress.php | 25 ++++++ .../Unit/SharedModel/Node/NodeAddressTest.php | 76 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php index 31532f5e14..1263558dc4 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php @@ -93,6 +93,11 @@ final public static function fromUriRepresentation(string $encoded): self return self::instance(json_decode(base64_decode($encoded), true)); } + final public function toUriRepresentation(): string + { + return base64_encode(json_encode($this->coordinates, JSON_THROW_ON_ERROR)); + } + /** * 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 4025758bd8..adbfff4d86 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Node/NodeAddress.php @@ -99,6 +99,31 @@ public function toJson(): string } } + public static function fromUriString(string $encoded): self + { + $parts = explode('__', $encoded); + if (count($parts) !== 4) { + throw new \RuntimeException(sprintf('Failed to decode NodeAddress from uri string: %s', $encoded), 1716051847); + } + return new self( + ContentRepositoryId::fromString($parts[0]), + WorkspaceName::fromString($parts[1]), + DimensionSpacePoint::fromUriRepresentation($parts[2]), + NodeAggregateId::fromString($parts[3]) + ); + } + + public function toUriString(): string + { + return sprintf( + '%s__%s__%s__%s', + $this->contentRepositoryId->value, + $this->workspaceName->value, + $this->dimensionSpacePoint->toUriRepresentation(), + $this->aggregateId->value + ); + } + public function jsonSerialize(): mixed { return get_object_vars($this); 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 0000000000..34f105d10b --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php @@ -0,0 +1,76 @@ + [ + 'nodeAddress' => NodeAddress::create( + ContentRepositoryId::fromString('default'), + WorkspaceName::forLive(), + DimensionSpacePoint::createWithoutDimensions(), + NodeAggregateId::fromString('marcus-heinrichus') + ), + 'serialized' => 'default__live__W10=__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' => 'default__user-mh__eyJsYW5ndWFnZSI6ImRlIn0=__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' => 'second__user-mh__eyJsYW5ndWFnZSI6ImVuX1VTIiwiYXVkaWVuY2UiOiJuaWNlIHBlb3BsZSJ9__my-node-id' + ]; + } + + /** + * @dataProvider urlCompatibleSerialization + */ + public function testUrlCompatibleSerialization(NodeAddress $nodeAddress, string $expected): void + { + self::assertEquals($expected, $nodeAddress->toUriString()); + } + + /** + * @dataProvider urlCompatibleSerialization + */ + public function testUrlCompatibleDeserialization(NodeAddress $expectedNodeAddress, string $encoded): void + { + $nodeAddress = NodeAddress::fromUriString($encoded); + self::assertInstanceOf(NodeAddress::class, $nodeAddress); + self::assertTrue($expectedNodeAddress->equals($nodeAddress)); + } +} From 9cfc16f2b9878d74c3f55a9a2e02647912e52de5 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 18 May 2024 19:50:29 +0200 Subject: [PATCH 04/19] TASK: Use new NodeAddress in preview action --- .../Classes/Controller/Frontend/NodeController.php | 3 +-- Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php | 12 +----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index f86a3c2dae..5d23d04e4c 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -133,8 +133,7 @@ public function previewAction(string $node): void $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); - // todo temporary legacy - $nodeAddress = NodeAddressFactory::create($contentRepository)->createCoreNodeAddressFromLegacyUriString($node); + $nodeAddress = NodeAddress::fromUriString($node); $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index 212df0c5b9..9ba4d15825 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -121,18 +121,8 @@ private function toShowActionRouteValues(NodeUriSpecification $specification): a */ private function toPreviewActionRouteValues(NodeUriSpecification $specification): array { - // todo fully migrate me to NodeUriSpecification - $reg = \Neos\Flow\Core\Bootstrap::$staticObjectManager->get(\Neos\ContentRepositoryRegistry\ContentRepositoryRegistry::class); - $ws = $reg->get($specification->node->contentRepositoryId)->getWorkspaceFinder()->findOneByName($specification->node->workspaceName); - $nodeAddress = new NodeAddress( - $ws->currentContentStreamId ?? throw new \Exception(), - $specification->node->dimensionSpacePoint, - $specification->node->aggregateId, - $specification->node->workspaceName, - ); - $routeValues = $specification->routingArguments; - $routeValues['node'] = $nodeAddress->serializeForUri(); + $routeValues['node'] = $specification->node->toUriString(); $routeValues['@action'] = strtolower('preview'); $routeValues['@controller'] = strtolower('Frontend\Node'); $routeValues['@package'] = strtolower('Neos.Neos'); From c1a33c159bf9fc2b9bb664d423c3127b60379c84 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 19 May 2024 09:52:35 +0200 Subject: [PATCH 05/19] TASK: Use NodeAddress Uri serialization instead of json format for routePartHandler --- .../Classes/Controller/Frontend/NodeController.php | 2 +- .../EventSourcedFrontendNodeRoutePartHandler.php | 11 ++++++----- .../Classes/Routing/NodeIdentityConverterAspect.php | 4 ++-- .../Behavior/Features/Bootstrap/RoutingTrait.php | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 5d23d04e4c..2c645bd043 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -195,7 +195,7 @@ public function previewAction(string $node): void */ public function showAction(string $node): void { - $nodeAddress = NodeAddress::fromJsonString($node); + $nodeAddress = NodeAddress::fromUriString($node); unset($node); if (!$nodeAddress->workspaceName->isLive()) { diff --git a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php index 4a9ebf3095..a1d319030a 100644 --- a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php +++ b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php @@ -28,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; @@ -202,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( @@ -247,7 +248,7 @@ private function matchUriPath( $dimensionSpacePoint, $nodeInfo->getNodeAggregateId(), ); - return new MatchResult($nodeAddress->toJson(), $nodeInfo->getRouteTags()); + return new MatchResult($nodeAddress->toUriString(), $nodeInfo->getRouteTags()); } /** @@ -268,7 +269,7 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para } try { - $resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult); + $resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName); } catch (NodeNotFoundException | InvalidShortcutException $exception) { // TODO log exception return false; @@ -290,7 +291,7 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para */ private function resolveNodeAddress( NodeAddress $nodeAddress, - SiteDetectionResult $currentRequestSiteDetectionResult + SiteNodeName $currentRequestSiteNodeName ): ResolveResult { $contentRepository = $this->contentRepositoryRegistry->get( $nodeAddress->contentRepositoryId @@ -315,7 +316,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/Routing/NodeIdentityConverterAspect.php b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php index 9a4250915e..d688417a79 100644 --- a/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php +++ b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php @@ -44,9 +44,9 @@ public function convertNodeToContextPathForRouting(JoinPointInterface $joinPoint { $objectArgument = $joinPoint->getMethodArgument('object'); if ($objectArgument instanceof Node) { - return ['__contextNodePath' => NodeAddress::fromNode($objectArgument)->toJson()]; + return ['__contextNodePath' => NodeAddress::fromNode($objectArgument)->toUriString()]; } elseif ($objectArgument instanceof NodeAddress) { - return ['__contextNodePath' => $objectArgument->toJson()]; + return ['__contextNodePath' => $objectArgument->toUriString()]; } elseif ($objectArgument instanceof LegacyNodeAddress) { return ['__contextNodePath' => $objectArgument->serializeForUri()]; } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 819aabe944..3ee919c23f 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -205,7 +205,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?->toJson() ?? '- none -'); + Assert::assertNull($matchedNodeAddress, 'Expected no node to be found, but instead the following node address was matched: ' . $matchedNodeAddress?->toUriString() ?? '- none -'); } /** @@ -244,7 +244,7 @@ private function match(UriInterface $uri): ?NodeAddress return null; } - return NodeAddress::fromJsonString($routeValues['node']); + return NodeAddress::fromUriString($routeValues['node']); } From 754f7357268879431e91feab92ead4de94e18cfb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 19 May 2024 10:53:43 +0200 Subject: [PATCH 06/19] TASK: Introduce NodeUri Options in favour of `NodeUriSpecification` The api will be ``` public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface ``` --- .../Controller/Frontend/NodeController.php | 6 +- .../{ => NodeUri}/NodeUriBuilder.php | 93 +++++++------------ .../{ => NodeUri}/NodeUriBuilderFactory.php | 2 +- .../FrontendRouting/NodeUri/Options.php | 64 +++++++++++++ .../FrontendRouting/NodeUriSpecification.php | 45 --------- .../Fusion/ConvertUrisImplementation.php | 14 +-- .../Classes/Fusion/Helper/LinkHelper.php | 5 +- .../Classes/Fusion/NodeUriImplementation.php | 17 ++-- .../ViewHelpers/Link/NodeViewHelper.php | 19 ++-- .../ViewHelpers/Uri/NodeViewHelper.php | 19 ++-- .../Features/Bootstrap/RoutingTrait.php | 5 +- 11 files changed, 140 insertions(+), 149 deletions(-) rename Neos.Neos/Classes/FrontendRouting/{ => NodeUri}/NodeUriBuilder.php (61%) rename Neos.Neos/Classes/FrontendRouting/{ => NodeUri}/NodeUriBuilderFactory.php (95%) create mode 100644 Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php delete mode 100644 Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 2c645bd043..f780e644e2 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -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\NodeAddressFactory; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Neos\View\FusionView; @@ -289,7 +287,7 @@ protected function handleShortcutNode(NodeAddress $nodeAddress): void } try { $resolvedUri = $this->nodeUriBuilderFactory->forRequest($this->request->getHttpRequest()) - ->uriFor(NodeUriSpecification::create($nodeAddress)); + ->uriFor($nodeAddress); } catch (NoMatchingRouteException $e) { throw new NodeNotFoundException(sprintf( 'The shortcut node target of node %s could not be resolved: %s', diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php similarity index 61% rename from Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php rename to Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 9ba4d15825..1b4ee87d1e 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -12,8 +12,9 @@ declare(strict_types=1); -namespace Neos\Neos\FrontendRouting; +namespace Neos\Neos\FrontendRouting\NodeUri; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Flow\Mvc\Routing\Dto\ResolveContext; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; @@ -43,25 +44,7 @@ public function __construct( * Return human readable host relative uris if the cr of the current request matches the one of the specified node. * For cross-links to another cr the resulting uri be absolute and contain the host of the other site's domain. * - * As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection) - * This method requires the node to be passed to be in the live workspace and will throw otherwise. - * - * @throws NoMatchingRouteException - */ - public function uriFor(NodeUriSpecification $specification): UriInterface - { - return $this->router->resolve( - new ResolveContext( - $this->baseUri, - $this->toShowActionRouteValues($specification), - false, - ltrim($this->baseUri->getPath(), '\/'), - $this->routeParameters - ) - ); - } - - /** + * absolute true: * Return human readable absolute uris with host, independent if the node is cross linked or of the current request. * For nodes of the current cr the passed base uri will be used as host. For cross-linked nodes the host will be derived by the site's domain. * @@ -70,13 +53,27 @@ public function uriFor(NodeUriSpecification $specification): UriInterface * * @throws NoMatchingRouteException */ - public function absoluteUriFor(NodeUriSpecification $specification): UriInterface + public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { + if (!$nodeAddress->workspaceName->isLive()) { + return $this->previewUriFor($nodeAddress, $options); + } + + $routeValues = $options?->routingArguments ?? []; + $routeValues['node'] = $nodeAddress; + $routeValues['@action'] = strtolower('show'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + if ($options?->format !== null && $options->format !== '') { + $routeValues['@format'] = $options->format; + } + return $this->router->resolve( new ResolveContext( $this->baseUri, - $this->toShowActionRouteValues($specification), - true, + $routeValues, + $options?->forceAbsolute ?? false, ltrim($this->baseUri->getPath(), '\/'), $this->routeParameters ) @@ -86,50 +83,26 @@ public function absoluteUriFor(NodeUriSpecification $specification): UriInterfac /** * Returns a host relative uri with fully qualified node as query parameter encoded. */ - public function previewUriFor(NodeUriSpecification $specification): UriInterface + public function previewUriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { + $routeValues = $options?->routingArguments ?? []; + $routeValues['node'] = $nodeAddress->toUriString(); + $routeValues['@action'] = strtolower('preview'); + $routeValues['@controller'] = strtolower('Frontend\Node'); + $routeValues['@package'] = strtolower('Neos.Neos'); + + if ($options?->format !== null && $options->format !== '') { + $routeValues['@format'] = $options->format; + } + return $this->router->resolve( new ResolveContext( $this->baseUri, - $this->toPreviewActionRouteValues($specification), - false, + $routeValues, + $options?->forceAbsolute ?? false, ltrim($this->baseUri->getPath(), '\/'), $this->routeParameters ) ); } - - /** - * @return array - */ - private function toShowActionRouteValues(NodeUriSpecification $specification): array - { - $routeValues = $specification->routingArguments; - $routeValues['node'] = $specification->node; - $routeValues['@action'] = strtolower('show'); - $routeValues['@controller'] = strtolower('Frontend\Node'); - $routeValues['@package'] = strtolower('Neos.Neos'); - - if ($specification->format !== '') { - $routeValues['@format'] = $specification->format; - } - return $routeValues; - } - - /** - * @return array - */ - private function toPreviewActionRouteValues(NodeUriSpecification $specification): array - { - $routeValues = $specification->routingArguments; - $routeValues['node'] = $specification->node->toUriString(); - $routeValues['@action'] = strtolower('preview'); - $routeValues['@controller'] = strtolower('Frontend\Node'); - $routeValues['@package'] = strtolower('Neos.Neos'); - - if ($specification->format !== '') { - $routeValues['@format'] = $specification->format; - } - return $routeValues; - } } diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php similarity index 95% rename from Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php rename to Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php index 7438d5c5d2..3bf8895e57 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\Neos\FrontendRouting; +namespace Neos\Neos\FrontendRouting\NodeUri; use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\Helper\RequestInformationHelper; diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php new file mode 100644 index 0000000000..7c1c64021a --- /dev/null +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php @@ -0,0 +1,64 @@ + $routingArguments + */ + private function __construct( + public ?bool $forceAbsolute, + public ?string $format, + public ?array $routingArguments, + ) { + } + + /** + * Creates an instance with the specified options + * + * Note: The signature of this method might be extended in the future, so it should always be used with named arguments + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + * + * @param array $routingArguments + */ + public static function create( + bool $forceAbsolute = null, + string $format = null, + array $routingArguments = null, + ): self { + return new self($forceAbsolute, $format, $routingArguments); + } + + /** + * Returns a new instance with the specified additional options + * + * Note: The signature of this method might be extended in the future, so it should always be used with named arguments + * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + * + * @param array $routingArguments + */ + public function with( + bool $forceAbsolute = null, + string $format = null, + array $routingArguments = null, + ): self { + return self::create( + $forceAbsolute ?? $this->forceAbsolute, + $format ?? $this->format, + $routingArguments ?? $this->routingArguments, + ); + } +} diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php b/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php deleted file mode 100644 index d586247796..0000000000 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriSpecification.php +++ /dev/null @@ -1,45 +0,0 @@ - $routingArguments - */ - private function __construct( - public NodeAddress $node, - public string $format, - public array $routingArguments, - ) { - } - - public static function create(NodeAddress $node): self - { - return new self($node, '', []); - } - - public function withFormat(string $format): self - { - return new self($this->node, $format, $this->routingArguments); - } - - /** - * @deprecated if you meant to append query parameters, - * please use {@see \Neos\Flow\Http\UriHelper::withAdditionalQueryParameters} instead: - * - * ```php - * UriHelper::withAdditionalQueryParameters($this->nodeUriBuilder->uriFor(...), ['q' => 'search term']); - * ``` - * - * @param array $routingArguments - */ - public function withRoutingArguments(array $routingArguments): self - { - return new self($this->node, $this->format, $routingArguments); - } -} diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 23e4aca025..90c7dfef93 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -29,8 +29,8 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Exception as NeosException; use Neos\Neos\Domain\Model\RenderingMode; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUri\Options; use Neos\Neos\Fusion\Cache\CacheTag; use Psr\Log\LoggerInterface; @@ -142,9 +142,11 @@ public function evaluate() $nodeAddress = NodeAddress::fromNode($node); $unresolvedUris = []; - $absolute = $this->fusionValue('absolute'); + $options = Options::create( + forceAbsolute: $this->fusionValue('absolute'), + ); - $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($nodeAddress, &$unresolvedUris, $absolute) { + $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($nodeAddress, &$unresolvedUris, $options) { $resolvedUri = null; switch ($matches[1]) { case 'node': @@ -162,9 +164,7 @@ public function evaluate() $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); } try { - $resolvedUri = $absolute - ? (string)$nodeUriBuilder->absoluteUriFor(NodeUriSpecification::create($nodeAddress)) - : (string)$nodeUriBuilder->uriFor(NodeUriSpecification::create($nodeAddress)); + $resolvedUri = (string)$nodeUriBuilder->uriFor($nodeAddress, $options); } catch (NoMatchingRouteException) { // todo log also arguments? $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $matches[0]), LogEnvironment::fromMethodName(__METHOD__)); diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index a4c246df72..9262f2c552 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -27,8 +27,7 @@ use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; use Neos\Neos\Fusion\ConvertUrisImplementation; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -111,7 +110,7 @@ public function resolveNodeUri( $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($controllerContext->getRequest()->getHttpRequest()); try { - $targetUri = $nodeUriBuilder->uriFor(NodeUriSpecification::create(NodeAddress::fromNode($targetNode))); + $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode)); } catch (NoMatchingRouteException $e) { $this->systemLogger->info(sprintf( 'Failed to build URI for node "%s": %e', diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 5a0f08d416..77247c20b5 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -23,8 +23,8 @@ use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUri\Options; use Psr\Log\LoggerInterface; /** @@ -150,14 +150,15 @@ public function evaluate() // https://github.com/neos/flow-development-collection/pull/2744 $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); } - $specification = NodeUriSpecification::create(NodeAddress::fromNode($node)) - ->withFormat($this->getFormat() ?: '') - ->withRoutingArguments($this->getAdditionalParams()); + + $options = Options::create( + forceAbsolute: $this->isAbsolute(), + format: $this->getFormat(), + routingArguments: $this->getAdditionalParams() + ); try { - $resolvedUri = $this->isAbsolute() - ? $nodeUriBuilder->absoluteUriFor($specification) - : $nodeUriBuilder->uriFor($specification); + $resolvedUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($node), $options); } catch (NoMatchingRouteException) { // todo log arguments? $this->systemLogger->warning(sprintf('Could not resolve "%s" to a node uri.', $node->aggregateId->value), LogEnvironment::fromMethodName(__METHOD__)); diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 6aefb4bf9d..74721f377a 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -32,8 +32,8 @@ use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUri\Options; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** @@ -302,14 +302,15 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); $uri = ''; - $specification = NodeUriSpecification::create($nodeAddress) - ->withFormat($this->arguments['format'] ?: '') - ->withRoutingArguments($this->arguments['arguments']); - try { - $uri = $this->arguments['absolute'] - ? $nodeUriBuilder->absoluteUriFor($specification) - : $nodeUriBuilder->uriFor($specification); + $uri = $nodeUriBuilder->uriFor( + $nodeAddress, + Options::create( + forceAbsolute: $this->arguments['absolute'], + format: $this->arguments['format'], + routingArguments: $this->arguments['arguments'] + ) + ); if ($this->arguments['section'] !== '') { $uri = $uri->withFragment($this->arguments['section']); diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 2f249e5cf0..86481d82e7 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -28,8 +28,8 @@ use Neos\FluidAdaptor\Core\ViewHelper\Exception as ViewHelperException; use Neos\Fusion\ViewHelpers\FusionContextTrait; use Neos\Neos\Domain\Service\NodeTypeNameFactory; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUri\Options; /** * A view helper for creating URIs pointing to nodes. @@ -214,14 +214,15 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); $uri = ''; - $specification = NodeUriSpecification::create($nodeAddress) - ->withFormat($this->arguments['format'] ?: '') - ->withRoutingArguments($this->arguments['arguments']); - try { - $uri = $this->arguments['absolute'] - ? $nodeUriBuilder->absoluteUriFor($specification) - : $nodeUriBuilder->uriFor($specification); + $uri = $nodeUriBuilder->uriFor( + $nodeAddress, + Options::create( + forceAbsolute: $this->arguments['absolute'], + format: $this->arguments['format'], + routingArguments: $this->arguments['arguments'] + ) + ); if ($this->arguments['section'] !== '') { $uri = $uri->withFragment($this->arguments['section']); diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 3ee919c23f..54029a738a 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -42,8 +42,7 @@ use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\FrontendRouting\DimensionResolution\DimensionResolverFactoryInterface; use Neos\Neos\FrontendRouting\DimensionResolution\RequestToDimensionSpacePointContext; -use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUriSpecification; +use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjectionFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionMiddleware; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; @@ -320,7 +319,7 @@ private function resolveUrl(string $nodeAggregateId, string $contentStreamId, st return $this->getObject(NodeUriBuilderFactory::class) ->forRequest($httpRequest) - ->uriFor(NodeUriSpecification::create($nodeAddress)); + ->uriFor($nodeAddress); } private function addRoutingParameters(ServerRequestInterface $httpRequest): ServerRequestInterface From 3ebf6bb36eca66968838a6639b93b9ea3cc0b617 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 23 May 2024 17:40:20 +0200 Subject: [PATCH 07/19] TASK: Serialize NodeAddress as json for uris --- .../DimensionSpace/DimensionSpacePoint.php | 10 ----- .../Classes/SharedModel/Node/NodeAddress.php | 40 +++++-------------- .../Unit/SharedModel/Node/NodeAddressTest.php | 22 +++++----- .../Controller/Frontend/NodeController.php | 4 +- ...entSourcedFrontendNodeRoutePartHandler.php | 2 +- .../FrontendRouting/NodeAddressFactory.php | 2 +- .../NodeUri/NodeUriBuilder.php | 2 +- .../Routing/NodeIdentityConverterAspect.php | 4 +- .../Features/Bootstrap/RoutingTrait.php | 4 +- 9 files changed, 31 insertions(+), 59 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php index 1263558dc4..4858d720f8 100644 --- a/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php +++ b/Neos.ContentRepository.Core/Classes/DimensionSpace/DimensionSpacePoint.php @@ -88,16 +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)); - } - - final public function toUriRepresentation(): string - { - return base64_encode(json_encode($this->coordinates, JSON_THROW_ON_ERROR)); - } - /** * 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 adbfff4d86..fcce795b58 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 @@ -99,31 +104,6 @@ public function toJson(): string } } - public static function fromUriString(string $encoded): self - { - $parts = explode('__', $encoded); - if (count($parts) !== 4) { - throw new \RuntimeException(sprintf('Failed to decode NodeAddress from uri string: %s', $encoded), 1716051847); - } - return new self( - ContentRepositoryId::fromString($parts[0]), - WorkspaceName::fromString($parts[1]), - DimensionSpacePoint::fromUriRepresentation($parts[2]), - NodeAggregateId::fromString($parts[3]) - ); - } - - public function toUriString(): string - { - return sprintf( - '%s__%s__%s__%s', - $this->contentRepositoryId->value, - $this->workspaceName->value, - $this->dimensionSpacePoint->toUriRepresentation(), - $this->aggregateId->value - ); - } - public function jsonSerialize(): mixed { return get_object_vars($this); diff --git a/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php b/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php index 34f105d10b..9c7de0cbad 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/SharedModel/Node/NodeAddressTest.php @@ -23,7 +23,7 @@ class NodeAddressTest extends TestCase { - public static function urlCompatibleSerialization(): iterable + public static function jsonSerialization(): iterable { yield 'no dimensions' => [ 'nodeAddress' => NodeAddress::create( @@ -32,7 +32,7 @@ public static function urlCompatibleSerialization(): iterable DimensionSpacePoint::createWithoutDimensions(), NodeAggregateId::fromString('marcus-heinrichus') ), - 'serialized' => 'default__live__W10=__marcus-heinrichus' + 'serialized' => '{"contentRepositoryId":"default","workspaceName":"live","dimensionSpacePoint":[],"aggregateId":"marcus-heinrichus"}' ]; yield 'one dimension' => [ @@ -42,7 +42,7 @@ public static function urlCompatibleSerialization(): iterable DimensionSpacePoint::fromArray(['language' => 'de']), NodeAggregateId::fromString('79e69d1c-b079-4535-8c8a-37e76736c445') ), - 'serialized' => 'default__user-mh__eyJsYW5ndWFnZSI6ImRlIn0=__79e69d1c-b079-4535-8c8a-37e76736c445' + 'serialized' => '{"contentRepositoryId":"default","workspaceName":"user-mh","dimensionSpacePoint":{"language":"de"},"aggregateId":"79e69d1c-b079-4535-8c8a-37e76736c445"}' ]; yield 'two dimensions' => [ @@ -52,24 +52,26 @@ public static function urlCompatibleSerialization(): iterable DimensionSpacePoint::fromArray(['language' => 'en_US', 'audience' => 'nice people']), NodeAggregateId::fromString('my-node-id') ), - 'serialized' => 'second__user-mh__eyJsYW5ndWFnZSI6ImVuX1VTIiwiYXVkaWVuY2UiOiJuaWNlIHBlb3BsZSJ9__my-node-id' + 'serialized' => '{"contentRepositoryId":"second","workspaceName":"user-mh","dimensionSpacePoint":{"language":"en_US","audience":"nice people"},"aggregateId":"my-node-id"}' ]; } /** - * @dataProvider urlCompatibleSerialization + * @dataProvider jsonSerialization + * @test */ - public function testUrlCompatibleSerialization(NodeAddress $nodeAddress, string $expected): void + public function serialization(NodeAddress $nodeAddress, string $expected): void { - self::assertEquals($expected, $nodeAddress->toUriString()); + self::assertEquals($expected, $nodeAddress->toJson()); } /** - * @dataProvider urlCompatibleSerialization + * @dataProvider jsonSerialization + * @test */ - public function testUrlCompatibleDeserialization(NodeAddress $expectedNodeAddress, string $encoded): void + public function deserialization(NodeAddress $expectedNodeAddress, string $encoded): void { - $nodeAddress = NodeAddress::fromUriString($encoded); + $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 f780e644e2..4045fa7702 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -131,7 +131,7 @@ public function previewAction(string $node): void $siteDetectionResult = SiteDetectionResult::fromRequest($this->request->getHttpRequest()); $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); - $nodeAddress = NodeAddress::fromUriString($node); + $nodeAddress = NodeAddress::fromJsonString($node); $subgraph = $contentRepository->getContentGraph($nodeAddress->workspaceName)->getSubgraph( $nodeAddress->dimensionSpacePoint, @@ -193,7 +193,7 @@ public function previewAction(string $node): void */ public function showAction(string $node): void { - $nodeAddress = NodeAddress::fromUriString($node); + $nodeAddress = NodeAddress::fromJsonString($node); unset($node); if (!$nodeAddress->workspaceName->isLive()) { diff --git a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php index a1d319030a..0a2a286398 100644 --- a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php +++ b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php @@ -248,7 +248,7 @@ private function matchUriPath( $dimensionSpacePoint, $nodeInfo->getNodeAggregateId(), ); - return new MatchResult($nodeAddress->toUriString(), $nodeInfo->getRouteTags()); + return new MatchResult($nodeAddress->toJson(), $nodeInfo->getRouteTags()); } /** diff --git a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php index 55aa6654a6..9bc511fdcf 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeAddressFactory.php @@ -90,7 +90,7 @@ public function createFromUriString(string $serializedNodeAddress): LegacyNodeAd 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) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 1b4ee87d1e..48833b4485 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -86,7 +86,7 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn public function previewUriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { $routeValues = $options?->routingArguments ?? []; - $routeValues['node'] = $nodeAddress->toUriString(); + $routeValues['node'] = $nodeAddress->toJson(); $routeValues['@action'] = strtolower('preview'); $routeValues['@controller'] = strtolower('Frontend\Node'); $routeValues['@package'] = strtolower('Neos.Neos'); diff --git a/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php index d688417a79..9a4250915e 100644 --- a/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php +++ b/Neos.Neos/Classes/Routing/NodeIdentityConverterAspect.php @@ -44,9 +44,9 @@ public function convertNodeToContextPathForRouting(JoinPointInterface $joinPoint { $objectArgument = $joinPoint->getMethodArgument('object'); if ($objectArgument instanceof Node) { - return ['__contextNodePath' => NodeAddress::fromNode($objectArgument)->toUriString()]; + return ['__contextNodePath' => NodeAddress::fromNode($objectArgument)->toJson()]; } elseif ($objectArgument instanceof NodeAddress) { - return ['__contextNodePath' => $objectArgument->toUriString()]; + return ['__contextNodePath' => $objectArgument->toJson()]; } elseif ($objectArgument instanceof LegacyNodeAddress) { return ['__contextNodePath' => $objectArgument->serializeForUri()]; } diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 54029a738a..541b242a40 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -204,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?->toUriString() ?? '- none -'); + Assert::assertNull($matchedNodeAddress, 'Expected no node to be found, but instead the following node address was matched: ' . $matchedNodeAddress?->toJson() ?? '- none -'); } /** @@ -243,7 +243,7 @@ private function match(UriInterface $uri): ?NodeAddress return null; } - return NodeAddress::fromUriString($routeValues['node']); + return NodeAddress::fromJsonString($routeValues['node']); } From f17587304d0ce4dd991fba4e7c3c842cc08dccbb Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 25 May 2024 13:40:33 +0200 Subject: [PATCH 08/19] TASK: Fix preview uri building to ignore custom options the custom options will likely be unintended as `uriFor` uses preview uris for non-live uris. Such preview route will likely not exist, and thus it will just provoke hard to debug errors. --- .../Classes/FrontendRouting/NodeUri/NodeUriBuilder.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 48833b4485..fff1b10fa9 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -82,19 +82,18 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn /** * Returns a host relative uri with fully qualified node as query parameter encoded. + * + * Note that only the option {@see Options::$forceAbsolute} is supported. */ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { - $routeValues = $options?->routingArguments ?? []; + $routeValues = []; + // todo use withQuery instead $routeValues['node'] = $nodeAddress->toJson(); $routeValues['@action'] = strtolower('preview'); $routeValues['@controller'] = strtolower('Frontend\Node'); $routeValues['@package'] = strtolower('Neos.Neos'); - if ($options?->format !== null && $options->format !== '') { - $routeValues['@format'] = $options->format; - } - return $this->router->resolve( new ResolveContext( $this->baseUri, From e855be8b493945b3b05166957e207ab0df308db3 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 25 May 2024 21:15:54 +0200 Subject: [PATCH 09/19] TASK: Document NodeUriBuilder and align behaviour to flows UriBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use flows base uri if configured for sub dir installations - Pass `uriPathPrefix` correctly along when base uri is configured - support for `FLOW_REWRITEURLS === ß` is not planned as this option is to be removed --- .../NodeUri/NodeUriBuilder.php | 79 +++++++++++++++---- .../NodeUri/NodeUriBuilderFactory.php | 29 +++++-- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index fff1b10fa9..57fadb2d1e 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -15,42 +15,78 @@ namespace Neos\Neos\FrontendRouting\NodeUri; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; +use Neos\Flow\Annotations as Flow; +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\Neos\FrontendRouting\Projection\DocumentUriPathProjection; +use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Psr\Http\Message\UriInterface; +#[Flow\Proxy(false)] final class NodeUriBuilder { /** - * @internal + * Please inject and use the {@see NodeUriBuilderFactory} to acquire this uri builder * - * This context ($baseUri and $routeParameters) can be inferred from the current request. + * #[Flow\Inject] + * protected NodeUriBuilderFactory $nodeUriBuilderFactory; * - * For generating node uris in cli context, you can leverage `fromBaseUri` and pass in the desired base uri, - * Wich will be used for when generating host absolute uris. - * If the base uri does not contain a host, absolute uris which would contain the host of the current request - * like from `absoluteUriFor`, will be generated without host. + * $this->nodeUriBuilderFactory->forRequest($someHttpRequest); + * + * @internal must not be manually instantiated but its factory must be used */ public function __construct( private readonly 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 readonly 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 readonly 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 readonly RouteParameters $routeParameters ) { } /** - * Return human readable host relative uris if the cr of the current request matches the one of the specified node. - * For cross-links to another cr the resulting uri be absolute and contain the host of the other site's domain. + * 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 DocumentUriPathProjection} + * Preview uris are build for other workspaces {@see previewUriFor} + * + * Cross-linking nodes + * ------------------- + * + * Cross linking to a node happens when the side 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}. + * + * Host relative urls are build by default for non cross-linked nodes. + * + * Supported options + * ----------------- * - * absolute true: - * Return human readable absolute uris with host, independent if the node is cross linked or of the current request. - * For nodes of the current cr the passed base uri will be used as host. For cross-linked nodes the host will be derived by the site's domain. + * forceAbsolute: + * Absolute urls for non cross-linked nodes can be enforced via {@see Options::$forceAbsolute}. + * In which case the base uri determined by the request is used as host instead of a possibly configured site domain's host. * - * As the human readable uris are only routed for nodes of the live workspace (see DocumentUriProjection) - * This method requires the node to be passed to be in the live workspace and will throw otherwise. + * format: + * todo * + * routingArguments: + * todo + * + * Note that appending additional query parameters can be done via {@see UriHelper::uriWithAdditionalQueryParameters()} + * + * @api * @throws NoMatchingRouteException */ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface @@ -74,16 +110,25 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn $this->baseUri, $routeValues, $options?->forceAbsolute ?? false, - ltrim($this->baseUri->getPath(), '\/'), + $this->uriPathPrefix, $this->routeParameters ) ); } /** - * Returns a host relative uri with fully qualified node as query parameter encoded. + * Returns an uri with json encoded node address as query parameter. + * + * Supported options + * ----------------- * - * Note that only the option {@see Options::$forceAbsolute} is supported. + * forceAbsolute: + * Absolute urls can be build via {@see Options::$forceAbsolute}, by default host relative urls will be build. + * + * Note that other options are not considered for preview uri building. + * + * @api + * @throws NoMatchingRouteException */ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { @@ -99,7 +144,7 @@ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null) $this->baseUri, $routeValues, $options?->forceAbsolute ?? false, - ltrim($this->baseUri->getPath(), '\/'), + $this->uriPathPrefix, $this->routeParameters ) ); diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php index 3bf8895e57..343c7a67c6 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php @@ -4,6 +4,7 @@ namespace Neos\Neos\FrontendRouting\NodeUri; +use GuzzleHttp\Psr7\Uri; use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\Helper\RequestInformationHelper; use Neos\Flow\Http\ServerRequestAttributes; @@ -14,17 +15,31 @@ #[Flow\Scope('singleton')] final class NodeUriBuilderFactory { - public function __construct( - private RouterInterface $router - ) { - } + /** + * The possibly configured Flow base URI, see {@see \Neos\Flow\Http\BaseUriProvider} + * @var string|null + */ + #[Flow\InjectConfiguration(package: 'Neos.Flow', path: 'http.baseUri')] + protected $configuredBaseUri; + + #[Flow\Inject] + protected RouterInterface $router; + /** + * @api + */ public function forRequest(ServerRequestInterface $request): NodeUriBuilder { - // TODO Flows base uri configuration is currently ignored - $baseUri = RequestInformationHelper::generateBaseUri($request); + $baseUri = $this->configuredBaseUri !== null + ? new Uri($this->configuredBaseUri) + : RequestInformationHelper::generateBaseUri($request); + $routeParameters = $request->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) ?? RouteParameters::createEmpty(); - return new NodeUriBuilder($this->router, $baseUri, $routeParameters); + + $uriPathPrefix = RequestInformationHelper::getScriptRequestPath($request); + $uriPathPrefix = ltrim($uriPathPrefix, '/'); + + return new NodeUriBuilder($this->router, $baseUri, $uriPathPrefix, $routeParameters); } } From 2c4670c130b14d484bb73308f1867b6f5306a4c9 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 11:14:42 +0200 Subject: [PATCH 10/19] TASK: Dont use `appendExceedingArguments` for preview action --- .../NodeUri/NodeUriBuilder.php | 23 ++++++++++++++----- Neos.Neos/Classes/Service/LinkingService.php | 21 +++++++++++++++-- Neos.Neos/Configuration/Routes.Frontend.yaml | 1 - 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 57fadb2d1e..3ad1bce09e 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -21,10 +21,19 @@ use Neos\Flow\Mvc\Routing\Dto\ResolveContext; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\RouterInterface; -use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjection; +use Neos\Flow\Mvc\Routing\UriBuilder; +use Neos\Neos\FrontendRouting\EventSourcedFrontendNodeRoutePartHandler; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Psr\Http\Message\UriInterface; +/** + * Neos abstraction to simplify node uri building. + * + * 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 + */ #[Flow\Proxy(false)] final class NodeUriBuilder { @@ -59,7 +68,7 @@ public function __construct( /** * 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 DocumentUriPathProjection} + * As the human-readable uris are only routed for nodes of the live workspace {@see EventSourcedFrontendNodeRoutePartHandler} * Preview uris are build for other workspaces {@see previewUriFor} * * Cross-linking nodes @@ -128,18 +137,16 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn * Note that other options are not considered for preview uri building. * * @api - * @throws NoMatchingRouteException + * @throws NoMatchingRouteException in the unlike case the preview route definition is misconfigured */ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { $routeValues = []; - // todo use withQuery instead - $routeValues['node'] = $nodeAddress->toJson(); $routeValues['@action'] = strtolower('preview'); $routeValues['@controller'] = strtolower('Frontend\Node'); $routeValues['@package'] = strtolower('Neos.Neos'); - return $this->router->resolve( + $previewActionUri = $this->router->resolve( new ResolveContext( $this->baseUri, $routeValues, @@ -148,5 +155,9 @@ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null) $this->routeParameters ) ); + return UriHelper::uriWithAdditionalQueryParameters( + $previewActionUri, + ['node' => $nodeAddress->toJson()] + ); } } diff --git a/Neos.Neos/Classes/Service/LinkingService.php b/Neos.Neos/Classes/Service/LinkingService.php index cd9348a536..fb520d6299 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/Configuration/Routes.Frontend.yaml b/Neos.Neos/Configuration/Routes.Frontend.yaml index d67077c2e6..164552278c 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' From ceb88ab0e67c74045cf6382048f0f01c6daf7b33 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 11:40:16 +0200 Subject: [PATCH 11/19] TASK: Don't use options for `previewUriFor` but always build absolute uri --- .../NodeUri/NodeUriBuilder.php | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 3ad1bce09e..6223b0b3ca 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -69,7 +69,7 @@ public function __construct( * 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} - * Preview uris are build for other workspaces {@see previewUriFor} + * Absolute preview uris are build for other workspaces {@see previewUriFor} * * Cross-linking nodes * ------------------- @@ -83,14 +83,16 @@ public function __construct( * Supported options * ----------------- * - * forceAbsolute: + * 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::$forceAbsolute}. * In which case the base uri determined by the request is used as host instead of a possibly configured site domain's host. * - * format: + * - format: * todo * - * routingArguments: + * - routingArguments: * todo * * Note that appending additional query parameters can be done via {@see UriHelper::uriWithAdditionalQueryParameters()} @@ -101,7 +103,7 @@ public function __construct( public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { if (!$nodeAddress->workspaceName->isLive()) { - return $this->previewUriFor($nodeAddress, $options); + return $this->previewUriFor($nodeAddress); } $routeValues = $options?->routingArguments ?? []; @@ -126,20 +128,16 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn } /** - * Returns an uri with json encoded node address as query parameter. - * - * Supported options - * ----------------- - * - * forceAbsolute: - * Absolute urls can be build via {@see Options::$forceAbsolute}, by default host relative urls will be build. + * Returns a host absolute uri with json encoded node address as query parameter. * - * Note that other options are not considered for preview uri building. + * 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, Options $options = null): UriInterface + public function previewUriFor(NodeAddress $nodeAddress): UriInterface { $routeValues = []; $routeValues['@action'] = strtolower('preview'); @@ -150,7 +148,7 @@ public function previewUriFor(NodeAddress $nodeAddress, Options $options = null) new ResolveContext( $this->baseUri, $routeValues, - $options?->forceAbsolute ?? false, + true, $this->uriPathPrefix, $this->routeParameters ) From 2afdf6b1d107e45ecb399e5aa5bcd820f1576120 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 11:40:54 +0200 Subject: [PATCH 12/19] TASK: Fix WorkspacesController to redirect to node uris correctly --- .../Management/WorkspacesController.php | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index 92f7fded38..f05455291b 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.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\NodeUri\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\PendingChangesProjection\ChangeFinder; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; @@ -81,6 +83,9 @@ class WorkspacesController extends AbstractModuleController #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; + #[Flow\Inject] protected SiteRepository $siteRepository; @@ -479,31 +484,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.Neos', - ['node' => $targetNodeAddressInPersonalWorkspace] + $this->redirectToUri( + $this->nodeUriBuilderFactory->forRequest($this->request->getHttpRequest()) + ->uriFor($targetNodeAddressInPersonalWorkspace) ); } @@ -826,7 +836,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, From 8887e77b073453d81f033141e46c567e20a410f7 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 14:01:28 +0200 Subject: [PATCH 13/19] TASK: Refine and document format and routingArguments node uri building options --- .../NodeUri/NodeUriBuilder.php | 23 +++-- .../FrontendRouting/NodeUri/Options.php | 97 +++++++++++++++---- .../Fusion/ConvertUrisImplementation.php | 4 +- .../Classes/Fusion/NodeUriImplementation.php | 12 ++- .../ViewHelpers/Link/NodeViewHelper.php | 17 ++-- .../ViewHelpers/Uri/NodeViewHelper.php | 17 ++-- 6 files changed, 120 insertions(+), 50 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 6223b0b3ca..cc2e0d65c3 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -87,32 +87,41 @@ public function __construct( * * - forceAbsolute: * Absolute urls for non cross-linked nodes can be enforced via {@see Options::$forceAbsolute}. - * In which case the base uri determined by the request is used as host instead of a possibly configured site domain's host. + * In which case the base uri determined by the request is used as host + * instead of a possibly configured site domain's host. * * - format: - * todo + * A custom format can be specified via {@see Options::withCustomFormat()} * * - routingArguments: - * todo + * Custom routing arguments can be specified via {@see Options::withCustomRoutingArguments()} * - * Note that appending additional query parameters can be done via {@see UriHelper::uriWithAdditionalQueryParameters()} + * Note that appending additional query parameters can be done + * via {@see UriHelper::uriWithAdditionalQueryParameters()}: + * + * UriHelper::withAdditionalQueryParameters( + * $this->nodeUriBuilder->uriFor(...), + * ['q' => 'search term'] + * ); * * @api * @throws NoMatchingRouteException */ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { + $options ??= Options::create(); + if (!$nodeAddress->workspaceName->isLive()) { return $this->previewUriFor($nodeAddress); } - $routeValues = $options?->routingArguments ?? []; + $routeValues = $options->routingArguments; $routeValues['node'] = $nodeAddress; $routeValues['@action'] = strtolower('show'); $routeValues['@controller'] = strtolower('Frontend\Node'); $routeValues['@package'] = strtolower('Neos.Neos'); - if ($options?->format !== null && $options->format !== '') { + if ($options->format !== '') { $routeValues['@format'] = $options->format; } @@ -120,7 +129,7 @@ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriIn new ResolveContext( $this->baseUri, $routeValues, - $options?->forceAbsolute ?? false, + $options->forceAbsolute, $this->uriPathPrefix, $this->routeParameters ) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php index 7c1c64021a..1c4902d55d 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php @@ -4,12 +4,12 @@ namespace Neos\Neos\FrontendRouting\NodeUri; -/* +/** * Immutable filter DTO for {@see NodeUriBuilder::uriFor()} * * Example: * - * Options::create(forceAbsolute: true); + * Options::create(forceAbsolute: true); * * @api for the factory methods; NOT for the inner state. */ @@ -20,9 +20,9 @@ * @param array $routingArguments */ private function __construct( - public ?bool $forceAbsolute, - public ?string $format, - public ?array $routingArguments, + public bool $forceAbsolute, + public string $format, + public array $routingArguments, ) { } @@ -31,15 +31,15 @@ private function __construct( * * Note: The signature of this method might be extended in the future, so it should always be used with named arguments * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments - * - * @param array $routingArguments */ public static function create( - bool $forceAbsolute = null, - string $format = null, - array $routingArguments = null, + bool $forceAbsolute = null ): self { - return new self($forceAbsolute, $format, $routingArguments); + return new self( + $forceAbsolute ?? false, + '', + [] + ); } /** @@ -47,18 +47,77 @@ public static function create( * * Note: The signature of this method might be extended in the future, so it should always be used with named arguments * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments - * - * @param array $routingArguments */ public function with( - bool $forceAbsolute = null, - string $format = null, - array $routingArguments = null, + bool $forceAbsolute = null ): self { - return self::create( + return new self( $forceAbsolute ?? $this->forceAbsolute, - $format ?? $this->format, - $routingArguments ?? $this->routingArguments, + $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 835e748c5a..64a51d858e 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -142,9 +142,7 @@ public function evaluate() $nodeAddress = NodeAddress::fromNode($node); $unresolvedUris = []; - $options = Options::create( - forceAbsolute: $this->fusionValue('absolute'), - ); + $options = Options::create(forceAbsolute: $this->fusionValue('absolute')); $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($nodeAddress, &$unresolvedUris, $options) { $resolvedUri = null; diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 77247c20b5..2898c206ef 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -151,11 +151,13 @@ public function evaluate() $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); } - $options = Options::create( - forceAbsolute: $this->isAbsolute(), - format: $this->getFormat(), - routingArguments: $this->getAdditionalParams() - ); + $options = Options::create(forceAbsolute: $this->isAbsolute()); + if ($format = $this->getFormat()) { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->getAdditionalParams()) { + $options = $options->withCustomRoutingArguments($routingArguments); + } try { $resolvedUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($node), $options); diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 74721f377a..b5d52e91ef 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -301,16 +301,17 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); + $options = Options::create(forceAbsolute: $this->arguments['absolute']); + if ($format = $this->arguments['format']) { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->arguments['arguments']) { + $options = $options->withCustomRoutingArguments($routingArguments); + } + $uri = ''; try { - $uri = $nodeUriBuilder->uriFor( - $nodeAddress, - Options::create( - forceAbsolute: $this->arguments['absolute'], - format: $this->arguments['format'], - routingArguments: $this->arguments['arguments'] - ) - ); + $uri = $nodeUriBuilder->uriFor($nodeAddress, $options); if ($this->arguments['section'] !== '') { $uri = $uri->withFragment($this->arguments['section']); diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 86481d82e7..0cc35be333 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -213,16 +213,17 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); + $options = Options::create(forceAbsolute: $this->arguments['absolute']); + if ($format = $this->arguments['format']) { + $options = $options->withCustomFormat($format); + } + if ($routingArguments = $this->arguments['arguments']) { + $options = $options->withCustomRoutingArguments($routingArguments); + } + $uri = ''; try { - $uri = $nodeUriBuilder->uriFor( - $nodeAddress, - Options::create( - forceAbsolute: $this->arguments['absolute'], - format: $this->arguments['format'], - routingArguments: $this->arguments['arguments'] - ) - ); + $uri = $nodeUriBuilder->uriFor($nodeAddress, $options); if ($this->arguments['section'] !== '') { $uri = $uri->withFragment($this->arguments['section']); From c34eb9b69060e24ebbcf40d8d1c1a48ec5d75668 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 14:13:05 +0200 Subject: [PATCH 14/19] TASK: Document when `NoMatchingRouteException` will be thrown --- .../FrontendRouting/NodeUri/NodeUriBuilder.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index cc2e0d65c3..b61b0e1c14 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -35,7 +35,7 @@ * @api except its constructor */ #[Flow\Proxy(false)] -final class NodeUriBuilder +final readonly class NodeUriBuilder { /** * Please inject and use the {@see NodeUriBuilderFactory} to acquire this uri builder @@ -48,20 +48,20 @@ final class NodeUriBuilder * @internal must not be manually instantiated but its factory must be used */ public function __construct( - private readonly RouterInterface $router, + 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 readonly UriInterface $baseUri, + 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 readonly string $uriPathPrefix, + 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 readonly RouteParameters $routeParameters + private RouteParameters $routeParameters ) { } @@ -105,7 +105,8 @@ public function __construct( * ); * * @api - * @throws NoMatchingRouteException + * @throws NoMatchingRouteException in the unlike case the default route definition is misconfigured, + * or more likely in combination with custom options but no backing route defined. */ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { From 95f1c5ef1e989786b4f570d8967ab56e3b38964c Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 28 May 2024 14:32:14 +0200 Subject: [PATCH 15/19] TASK: Further improve comments and exception handling --- ...entSourcedFrontendNodeRoutePartHandler.php | 5 +++-- .../NodeUri/NodeUriBuilder.php | 22 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php index 0a2a286398..ae240471fa 100644 --- a/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php +++ b/Neos.Neos/Classes/FrontendRouting/EventSourcedFrontendNodeRoutePartHandler.php @@ -270,8 +270,8 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para try { $resolveResult = $this->resolveNodeAddress($nodeAddress, $currentRequestSiteDetectionResult->siteNodeName); - } catch (NodeNotFoundException | InvalidShortcutException $exception) { - // TODO log exception + } catch (NodeNotFoundException | TargetSiteNotFoundException | InvalidShortcutException $exception) { + // TODO log exception ... yes todo return false; } @@ -288,6 +288,7 @@ public function resolveWithParameters(array &$routeValues, RouteParameters $para * * @throws InvalidShortcutException * @throws NodeNotFoundException + * @throws TargetSiteNotFoundException */ private function resolveNodeAddress( NodeAddress $nodeAddress, diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index b61b0e1c14..8c797be5f3 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -29,6 +29,10 @@ /** * Neos abstraction to simplify node uri building. * +* Builds URIs to nodes, taking workspace (live / shared / user) into account. + * 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. * @@ -80,6 +84,11 @@ public function __construct( * * 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 * ----------------- * @@ -105,14 +114,23 @@ public function __construct( * ); * * @api - * @throws NoMatchingRouteException in the unlike case the default route definition is misconfigured, - * or more likely in combination with custom options but no backing route defined. + * @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, Options $options = null): UriInterface { $options ??= Options::create(); 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); } From b08d2489d5b71c52786383ccd46ffbe1436fc450 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:40:28 +0200 Subject: [PATCH 16/19] TASK: Pass ActionRequest instead of destructuring HttpRequest see https://github.com/neos/flow-development-collection/issues/3354 --- .../Classes/Controller/Frontend/NodeController.php | 2 +- .../Module/Management/WorkspacesController.php | 2 +- .../FrontendRouting/NodeUri/NodeUriBuilder.php | 4 ++-- .../NodeUri/NodeUriBuilderFactory.php | 12 +++++++----- .../Classes/Fusion/ConvertUrisImplementation.php | 9 ++++----- Neos.Neos/Classes/Fusion/Helper/LinkHelper.php | 2 +- Neos.Neos/Classes/Fusion/NodeUriImplementation.php | 9 ++++----- .../Classes/ViewHelpers/Link/NodeViewHelper.php | 2 +- Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php | 2 +- .../Behavior/Features/Bootstrap/RoutingTrait.php | 2 +- 10 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 4045fa7702..45fef9d1e7 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -286,7 +286,7 @@ protected function handleShortcutNode(NodeAddress $nodeAddress): void return; } try { - $resolvedUri = $this->nodeUriBuilderFactory->forRequest($this->request->getHttpRequest()) + $resolvedUri = $this->nodeUriBuilderFactory->forActionRequest($this->request) ->uriFor($nodeAddress); } catch (NoMatchingRouteException $e) { throw new NodeNotFoundException(sprintf( diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index f05455291b..8e63f03d7c 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -512,7 +512,7 @@ public function rebaseAndRedirectAction(Node $targetNode, Workspace $targetWorks } $this->redirectToUri( - $this->nodeUriBuilderFactory->forRequest($this->request->getHttpRequest()) + $this->nodeUriBuilderFactory->forActionRequest($this->request) ->uriFor($targetNodeAddressInPersonalWorkspace) ); } diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php index 8c797be5f3..b90decb3b3 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php @@ -29,7 +29,7 @@ /** * Neos abstraction to simplify node uri building. * -* Builds URIs to nodes, taking workspace (live / shared / user) into account. + * Builds URIs to nodes, taking workspace (live / shared / user) into account. * 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) * @@ -78,7 +78,7 @@ public function __construct( * Cross-linking nodes * ------------------- * - * Cross linking to a node happens when the side determined based on the current + * 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}. * diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php index 343c7a67c6..bc6e0e6e75 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php @@ -8,10 +8,13 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Http\Helper\RequestInformationHelper; use Neos\Flow\Http\ServerRequestAttributes; +use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\RouterInterface; -use Psr\Http\Message\ServerRequestInterface; +/** + * @api + */ #[Flow\Scope('singleton')] final class NodeUriBuilderFactory { @@ -25,11 +28,10 @@ final class NodeUriBuilderFactory #[Flow\Inject] protected RouterInterface $router; - /** - * @api - */ - public function forRequest(ServerRequestInterface $request): NodeUriBuilder + public function forActionRequest(ActionRequest $actionRequest): NodeUriBuilder { + $request = $actionRequest->getHttpRequest(); + $baseUri = $this->configuredBaseUri !== null ? new Uri($this->configuredBaseUri) : RequestInformationHelper::generateBaseUri($request); diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 64a51d858e..05351d191c 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -153,13 +153,12 @@ public function evaluate() ); $possibleRequest = $this->runtime->fusionGlobals->get('request'); if ($possibleRequest instanceof ActionRequest) { - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($possibleRequest->getHttpRequest()); + $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 - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); + // https://github.com/neos/flow-development-collection/issues/3354 + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest(ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())); } try { $resolvedUri = (string)$nodeUriBuilder->uriFor($nodeAddress, $options); diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index 9262f2c552..e22d02c8e3 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -107,7 +107,7 @@ public function resolveNodeUri( return null; } - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($controllerContext->getRequest()->getHttpRequest()); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()); try { $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode)); diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 2898c206ef..480719d3a9 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -142,13 +142,12 @@ public function evaluate() $possibleRequest = $this->runtime->fusionGlobals->get('request'); if ($possibleRequest instanceof ActionRequest) { - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($possibleRequest->getHttpRequest()); + $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 - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest(ServerRequest::fromGlobals()); + // https://github.com/neos/flow-development-collection/issues/3354 + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest(ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())); } $options = Options::create(forceAbsolute: $this->isAbsolute()); diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index b5d52e91ef..fe20f98ac7 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -299,7 +299,7 @@ public function render(): string } } - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); $options = Options::create(forceAbsolute: $this->arguments['absolute']); if ($format = $this->arguments['format']) { diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 0cc35be333..5d45a95dae 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -211,7 +211,7 @@ public function render(): string ), 1601372376); } - $nodeUriBuilder = $this->nodeUriBuilderFactory->forRequest($this->controllerContext->getRequest()->getHttpRequest()); + $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); $options = Options::create(forceAbsolute: $this->arguments['absolute']); if ($format = $this->arguments['format']) { diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 541b242a40..90c66129cf 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -318,7 +318,7 @@ private function resolveUrl(string $nodeAggregateId, string $contentStreamId, st $httpRequest = $this->addRoutingParameters($httpRequest); return $this->getObject(NodeUriBuilderFactory::class) - ->forRequest($httpRequest) + ->forActionRequest(\Neos\Flow\Mvc\ActionRequest::fromHttpRequest($httpRequest)) ->uriFor($nodeAddress); } From 18f8724219072916193d3c0792c112bb290e18f0 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:37:21 +0200 Subject: [PATCH 17/19] TASK: Place UriBuilder directly in `FrontendRouting` --- Neos.Neos/Classes/Controller/Frontend/NodeController.php | 2 +- .../Controller/Module/Management/WorkspacesController.php | 2 +- .../Classes/FrontendRouting/{NodeUri => }/NodeUriBuilder.php | 3 +-- .../FrontendRouting/{NodeUri => }/NodeUriBuilderFactory.php | 2 +- Neos.Neos/Classes/FrontendRouting/{NodeUri => }/Options.php | 2 +- Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php | 4 ++-- Neos.Neos/Classes/Fusion/Helper/LinkHelper.php | 2 +- Neos.Neos/Classes/Fusion/NodeUriImplementation.php | 4 ++-- Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php | 4 ++-- Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php | 4 ++-- Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php | 2 +- 11 files changed, 15 insertions(+), 16 deletions(-) rename Neos.Neos/Classes/FrontendRouting/{NodeUri => }/NodeUriBuilder.php (98%) rename Neos.Neos/Classes/FrontendRouting/{NodeUri => }/NodeUriBuilderFactory.php (96%) rename Neos.Neos/Classes/FrontendRouting/{NodeUri => }/Options.php (98%) diff --git a/Neos.Neos/Classes/Controller/Frontend/NodeController.php b/Neos.Neos/Classes/Controller/Frontend/NodeController.php index 45fef9d1e7..0f2cbf16f3 100644 --- a/Neos.Neos/Classes/Controller/Frontend/NodeController.php +++ b/Neos.Neos/Classes/Controller/Frontend/NodeController.php @@ -40,7 +40,7 @@ use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; use Neos\Neos\View\FusionView; diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index 8e63f03d7c..8624ec21bb 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -65,7 +65,7 @@ use Neos\Neos\Domain\Workspace\WorkspaceProvider; use Neos\Neos\FrontendRouting\NodeAddress as LegacyNodeAddress; use Neos\Neos\FrontendRouting\NodeAddressFactory; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Neos\Neos\PendingChangesProjection\ChangeFinder; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php similarity index 98% rename from Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php rename to Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index b90decb3b3..a56b5b9a87 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\Neos\FrontendRouting\NodeUri; +namespace Neos\Neos\FrontendRouting; use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use Neos\Flow\Annotations as Flow; @@ -22,7 +22,6 @@ use Neos\Flow\Mvc\Routing\Dto\RouteParameters; use Neos\Flow\Mvc\Routing\RouterInterface; use Neos\Flow\Mvc\Routing\UriBuilder; -use Neos\Neos\FrontendRouting\EventSourcedFrontendNodeRoutePartHandler; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; use Psr\Http\Message\UriInterface; diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php similarity index 96% rename from Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php rename to Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php index bc6e0e6e75..be50caaa77 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/NodeUriBuilderFactory.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilderFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\Neos\FrontendRouting\NodeUri; +namespace Neos\Neos\FrontendRouting; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Annotations as Flow; diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php b/Neos.Neos/Classes/FrontendRouting/Options.php similarity index 98% rename from Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php rename to Neos.Neos/Classes/FrontendRouting/Options.php index 1c4902d55d..d82cd8a1a6 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUri/Options.php +++ b/Neos.Neos/Classes/FrontendRouting/Options.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Neos\Neos\FrontendRouting\NodeUri; +namespace Neos\Neos\FrontendRouting; /** * Immutable filter DTO for {@see NodeUriBuilder::uriFor()} diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 05351d191c..57ba809914 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -29,8 +29,8 @@ use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\Domain\Exception as NeosException; use Neos\Neos\Domain\Model\RenderingMode; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUri\Options; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Fusion\Cache\CacheTag; use Psr\Log\LoggerInterface; diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index e22d02c8e3..75ea5ed3f1 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -27,7 +27,7 @@ use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\Fusion\ConvertUrisImplementation; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 480719d3a9..5e4d2d847e 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -23,8 +23,8 @@ use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Mvc\Exception\NoMatchingRouteException; use Neos\Fusion\FusionObjects\AbstractFusionObject; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUri\Options; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Psr\Log\LoggerInterface; /** diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index fe20f98ac7..837ef3a3bf 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -32,8 +32,8 @@ use Neos\Neos\FrontendRouting\Exception\InvalidShortcutException; use Neos\Neos\FrontendRouting\Exception\NodeNotFoundException; use Neos\Neos\FrontendRouting\NodeShortcutResolver; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUri\Options; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; /** diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index 5d45a95dae..ad20391326 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -28,8 +28,8 @@ use Neos\FluidAdaptor\Core\ViewHelper\Exception as ViewHelperException; use Neos\Fusion\ViewHelpers\FusionContextTrait; use Neos\Neos\Domain\Service\NodeTypeNameFactory; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; -use Neos\Neos\FrontendRouting\NodeUri\Options; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; /** * A view helper for creating URIs pointing to nodes. diff --git a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php index 90c66129cf..a8c1599029 100644 --- a/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php +++ b/Neos.Neos/Tests/Behavior/Features/Bootstrap/RoutingTrait.php @@ -42,7 +42,7 @@ use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\FrontendRouting\DimensionResolution\DimensionResolverFactoryInterface; use Neos\Neos\FrontendRouting\DimensionResolution\RequestToDimensionSpacePointContext; -use Neos\Neos\FrontendRouting\NodeUri\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\Projection\DocumentUriPathProjectionFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionMiddleware; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; From 95fa4cbc9d88d67b8380946b5ff7225b93a1e048 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:10:53 +0200 Subject: [PATCH 18/19] TASK: Restore feature to keep current format of action request when building uris from Fusion or Fluid The magic of keeping the format either has helped (by knowing it beforehand) or worse lead to confusion and hard to debug uri building errors (the `@format: json` part in the exception dump tends to go unnoticed for the untrained eye) Still for this change it would be cruel to change behaviour like this so we reimplement it. --- .../Fusion/ConvertUrisImplementation.php | 25 +++++++++++-------- .../Classes/Fusion/Helper/LinkHelper.php | 8 +++++- .../Classes/Fusion/NodeUriImplementation.php | 3 ++- .../ViewHelpers/Link/NodeViewHelper.php | 3 ++- .../ViewHelpers/Uri/NodeViewHelper.php | 3 ++- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index 57ba809914..fc58a77a0e 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -144,22 +144,27 @@ public function evaluate() $unresolvedUris = []; $options = Options::create(forceAbsolute: $this->fusionValue('absolute')); - $processedContent = preg_replace_callback(self::PATTERN_SUPPORTED_URIS, function (array $matches) use ($nodeAddress, &$unresolvedUris, $options) { + $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 ($nodeAddress, &$unresolvedUris, $nodeUriBuilder, $options) { $resolvedUri = null; switch ($matches[1]) { case 'node': $nodeAddress = $nodeAddress->withAggregateId( NodeAggregateId::fromString($matches[2]) ); - $possibleRequest = $this->runtime->fusionGlobals->get('request'); - if ($possibleRequest instanceof ActionRequest) { - $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($possibleRequest); - } 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())); - } try { $resolvedUri = (string)$nodeUriBuilder->uriFor($nodeAddress, $options); } catch (NoMatchingRouteException) { diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index 75ea5ed3f1..dd04c0d2b6 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -28,6 +28,7 @@ use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Repository\AssetRepository; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Neos\Neos\Fusion\ConvertUrisImplementation; use Psr\Http\Message\UriInterface; use Psr\Log\LoggerInterface; @@ -109,8 +110,13 @@ public function resolveNodeUri( $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()); + $options = Options::create(); + $format = $controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { + $options = $options->withCustomFormat($format); + } try { - $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode)); + $targetUri = $nodeUriBuilder->uriFor(NodeAddress::fromNode($targetNode), $options); } catch (NoMatchingRouteException $e) { $this->systemLogger->info(sprintf( 'Failed to build URI for node "%s": %e', diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index 5e4d2d847e..a8327fee3d 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -151,7 +151,8 @@ public function evaluate() } $options = Options::create(forceAbsolute: $this->isAbsolute()); - if ($format = $this->getFormat()) { + $format = $this->getFormat() ?: $possibleRequest->getFormat(); + if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); } if ($routingArguments = $this->getAdditionalParams()) { diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 837ef3a3bf..040a976c51 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -302,7 +302,8 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); $options = Options::create(forceAbsolute: $this->arguments['absolute']); - if ($format = $this->arguments['format']) { + $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); } if ($routingArguments = $this->arguments['arguments']) { diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index ad20391326..bdbb7c42d6 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -214,7 +214,8 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); $options = Options::create(forceAbsolute: $this->arguments['absolute']); - if ($format = $this->arguments['format']) { + $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); + if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); } if ($routingArguments = $this->arguments['arguments']) { From a69fed4d7f41736ecc7be3c8f295c28a0f6b994d Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:30:13 +0200 Subject: [PATCH 19/19] TASK: Introduce dedicated `Options::withForceAbsolute` The filter like signatures dont feel quite right because they only have one argument ... and its a yagni to add new args --- .../FrontendRouting/NodeUriBuilder.php | 4 +- Neos.Neos/Classes/FrontendRouting/Options.php | 41 ++++++++++--------- .../Fusion/ConvertUrisImplementation.php | 2 +- .../Classes/Fusion/Helper/LinkHelper.php | 2 +- .../Classes/Fusion/NodeUriImplementation.php | 2 +- .../ViewHelpers/Link/NodeViewHelper.php | 2 +- .../ViewHelpers/Uri/NodeViewHelper.php | 2 +- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php index a56b5b9a87..71fbd46e4d 100644 --- a/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php +++ b/Neos.Neos/Classes/FrontendRouting/NodeUriBuilder.php @@ -94,7 +94,7 @@ public function __construct( * 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::$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. * @@ -123,7 +123,7 @@ public function __construct( */ public function uriFor(NodeAddress $nodeAddress, Options $options = null): UriInterface { - $options ??= Options::create(); + $options ??= Options::createEmpty(); if (!$nodeAddress->workspaceName->isLive()) { // we cannot build a human-readable uri using the showAction as diff --git a/Neos.Neos/Classes/FrontendRouting/Options.php b/Neos.Neos/Classes/FrontendRouting/Options.php index d82cd8a1a6..2a7d7a2b87 100644 --- a/Neos.Neos/Classes/FrontendRouting/Options.php +++ b/Neos.Neos/Classes/FrontendRouting/Options.php @@ -9,7 +9,7 @@ * * Example: * - * Options::create(forceAbsolute: true); + * Options::createForceAbsolute()->withCustomFormat('json'); * * @api for the factory methods; NOT for the inner state. */ @@ -27,32 +27,35 @@ private function __construct( } /** - * Creates an instance with the specified options + * 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(); * - * Note: The signature of this method might be extended in the future, so it should always be used with named arguments - * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments */ - public static function create( - bool $forceAbsolute = null - ): self { - return new self( - $forceAbsolute ?? false, - '', - [] - ); + public static function createForceAbsolute(): self + { + return new self(true, '', []); } /** - * Returns a new instance with the specified additional options + * Option to enforce absolute urls for non cross-linked nodes. * - * Note: The signature of this method might be extended in the future, so it should always be used with named arguments - * @see https://www.php.net/manual/en/functions.arguments.php#functions.named-arguments + * Absolute urls are fully qualified with protocol and host. */ - public function with( - bool $forceAbsolute = null - ): self { + public function withForceAbsolute(): self + { return new self( - $forceAbsolute ?? $this->forceAbsolute, + true, $this->format, $this->routingArguments ); diff --git a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php index fc58a77a0e..fc27ab0b5c 100644 --- a/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php +++ b/Neos.Neos/Classes/Fusion/ConvertUrisImplementation.php @@ -142,7 +142,7 @@ public function evaluate() $nodeAddress = NodeAddress::fromNode($node); $unresolvedUris = []; - $options = Options::create(forceAbsolute: $this->fusionValue('absolute')); + $options = $this->fusionValue('absolute') ? Options::createForceAbsolute() : Options::createEmpty(); $possibleRequest = $this->runtime->fusionGlobals->get('request'); if ($possibleRequest instanceof ActionRequest) { diff --git a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php index dd04c0d2b6..d6c92994f7 100644 --- a/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/LinkHelper.php @@ -110,7 +110,7 @@ public function resolveNodeUri( $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($controllerContext->getRequest()); - $options = Options::create(); + $options = Options::createEmpty(); $format = $controllerContext->getRequest()->getFormat(); if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); diff --git a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php index a8327fee3d..69ad10218f 100644 --- a/Neos.Neos/Classes/Fusion/NodeUriImplementation.php +++ b/Neos.Neos/Classes/Fusion/NodeUriImplementation.php @@ -150,7 +150,7 @@ public function evaluate() $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest(ActionRequest::fromHttpRequest(ServerRequest::fromGlobals())); } - $options = Options::create(forceAbsolute: $this->isAbsolute()); + $options = $this->isAbsolute() ? Options::createForceAbsolute() : Options::createEmpty(); $format = $this->getFormat() ?: $possibleRequest->getFormat(); if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); diff --git a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php index 040a976c51..d799064673 100644 --- a/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Link/NodeViewHelper.php @@ -301,7 +301,7 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); - $options = Options::create(forceAbsolute: $this->arguments['absolute']); + $options = $this->arguments['absolute'] ? Options::createForceAbsolute() : Options::createEmpty(); $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); if ($format && $format !== 'html') { $options = $options->withCustomFormat($format); diff --git a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php index bdbb7c42d6..79e7e997a2 100644 --- a/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php +++ b/Neos.Neos/Classes/ViewHelpers/Uri/NodeViewHelper.php @@ -213,7 +213,7 @@ public function render(): string $nodeUriBuilder = $this->nodeUriBuilderFactory->forActionRequest($this->controllerContext->getRequest()); - $options = Options::create(forceAbsolute: $this->arguments['absolute']); + $options = $this->arguments['absolute'] ? Options::createForceAbsolute() : Options::createEmpty(); $format = $this->arguments['format'] ?: $this->controllerContext->getRequest()->getFormat(); if ($format && $format !== 'html') { $options = $options->withCustomFormat($format);